# Storno.ro API Documentation — Full Content > This file contains the full content of all documentation pages for Storno.ro, an e-invoicing platform for EU businesses. Base URL: https://api.storno.ro Docs: https://docs.storno.ro --- ## Authentication > Learn how to authenticate with the Storno.ro API using JWT tokens, refresh tokens, Google OAuth, and passkeys. URL: https://docs.storno.ro/getting-started/authentication # Authentication The Storno.ro API uses JWT (JSON Web Tokens) for authentication. You can obtain tokens via email/password login, Google OAuth, or passkey (WebAuthn) authentication. ## Obtaining a Token ### Email & Password ```bash curl -X POST https://api.storno.ro/api/auth \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "password": "your_password" }' ``` ```js const response = await fetch('https://api.storno.ro/api/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'user@example.com', password: 'your_password' }) }); const { token, refresh_token } = await response.json(); ``` **Response:** ```json { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...", "refresh_token": "def50200a..." } ``` ### Google OAuth Exchange a Google ID token for API credentials: ```bash curl -X POST https://api.storno.ro/api/auth/google \ -H "Content-Type: application/json" \ -d '{ "idToken": "google_id_token_here" }' ``` **Response:** ```json { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...", "refresh_token": "def50200a...", "isNewUser": false } ``` If the user doesn't exist, an account is automatically created and `isNewUser` will be `true`. ### Passkey (WebAuthn) Passkey authentication is a two-step process: **Step 1: Request login options** ```bash curl -X POST https://api.storno.ro/api/auth/passkey/login/options \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com" }' ``` Returns a WebAuthn challenge that must be signed by the user's authenticator. **Step 2: Submit signed response** ```bash curl -X POST https://api.storno.ro/api/auth/passkey/login \ -H "Content-Type: application/json" \ -d '{ "response": { ... } }' ``` **Response:** ```json { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...", "refresh_token": "def50200a..." } ``` ## Multi-Factor Authentication (MFA) When a user has two-factor authentication enabled, login via email/password or Google OAuth returns an MFA challenge instead of tokens: ```json { "mfa_required": true, "mfa_token": "a1b2c3d4e5f6...", "mfa_methods": ["totp", "backup_code"] } ``` You must then complete the challenge by submitting a TOTP code or backup code: ```bash curl -X POST https://api.storno.ro/api/auth/mfa/verify \ -H "Content-Type: application/json" \ -d '{ "mfaToken": "a1b2c3d4e5f6...", "code": "123456", "type": "totp" }' ``` ```js const response = await fetch('https://api.storno.ro/api/auth/mfa/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mfaToken: 'a1b2c3d4e5f6...', code: '123456', type: 'totp', }), }); const { token, refresh_token } = await response.json(); ``` **Response:** ```json { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...", "refresh_token": "def50200a..." } ``` **Passkey logins skip MFA entirely.** Passkeys inherently satisfy multi-factor authentication (possession of the device + biometric or PIN), so no additional challenge is required. See the [MFA API reference](/api-reference/auth/mfa-verify) for details on challenge verification, and [MFA Status](/api-reference/auth/mfa-status) for managing MFA settings. ## Using the Token Include the JWT token in the `Authorization` header of every request: ```bash curl https://api.storno.ro/api/v1/me \ -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..." ``` ## API Keys For programmatic or long-lived access, you can use API keys instead of JWT tokens. API keys use scoped permissions and are ideal for CI/CD pipelines, scripts, and integrations. Create an API key via the [Create API token](/api-reference/api-keys/create) endpoint or from the web application under **Settings > API Keys**. API keys must be sent **without** the `Bearer` prefix. The `Bearer` prefix is reserved for JWT tokens. ```bash curl https://api.storno.ro/api/v1/invoices \ -H "Authorization: af_a1b2c3d4e5f6..." \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` API keys use the `af_` prefix for easy identification. The key's permissions are intersected with the user's role — both must grant access for a request to succeed. See [API Keys](/api-reference/api-keys/create) for details on creating and managing keys. ## Refreshing Tokens JWT tokens expire after a configured period. Use the refresh token to obtain a new JWT without re-authenticating: ```bash curl -X POST https://api.storno.ro/api/auth/token/refresh \ -H "Content-Type: application/json" \ -d '{ "refresh_token": "def50200a..." }' ``` **Response:** ```json { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...", "refresh_token": "def50200b..." } ``` Both the access token and refresh token are rotated on each refresh. Store the new refresh token for subsequent refreshes. ## Company Context Most API endpoints operate within a company context. You must include the `X-Company` header with the company UUID: ```bash curl https://api.storno.ro/api/v1/invoices \ -H "Authorization: Bearer {token}" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` To get the list of companies you have access to: ```bash curl https://api.storno.ro/api/v1/companies \ -H "Authorization: Bearer {token}" ``` ## Registering a Passkey Authenticated users can register passkeys for passwordless login: **Step 1: Get registration options** ```bash curl -X POST https://api.storno.ro/api/v1/passkey/register/options \ -H "Authorization: Bearer {token}" ``` **Step 2: Submit registration response** ```bash curl -X POST https://api.storno.ro/api/v1/passkey/register \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "name": "My MacBook", "response": { ... } }' ``` ## Managing Passkeys ```bash # List passkeys curl https://api.storno.ro/api/v1/me/passkeys \ -H "Authorization: Bearer {token}" # Delete a passkey curl -X DELETE https://api.storno.ro/api/v1/me/passkeys/{id} \ -H "Authorization: Bearer {token}" ``` ## Token Lifetime | Token | Lifetime | |-------|----------| | Access token (JWT) | Configured per deployment (typically 1 hour) | | Refresh token | Configured per deployment (typically 30 days) | ## Error Responses | Status | Description | |--------|-------------| | 401 | Invalid credentials or expired token | | 403 | Account is inactive or email not confirmed | | 429 | Too many login attempts (rate limited) | --- ## Environment Variables > Complete reference for all backend environment variables used in Storno.ro deployments. URL: https://docs.storno.ro/getting-started/environment-variables # Environment Variables Complete reference for configuring a Storno.ro deployment. All variables are set in `.env.local` (development) or passed as environment variables (Docker / production). --- ## Core | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `APP_SECRET` | Yes | — | Symfony application secret. Use a random 32+ character string. | | `APP_ENV` | No | `dev` | Environment: `dev`, `prod`, or `test` | | `DATABASE_URL` | Yes | — | MySQL connection string, e.g. `mysql://user:pass@127.0.0.1:3306/storno` | | `FRONTEND_URL` | Yes | — | Frontend URL for CORS origins and email links, e.g. `https://app.storno.ro` | | `PUBLIC_API_BASE` | Yes | — | How the browser reaches the API. For single-domain setups this is `FRONTEND_URL` + `/api` (e.g. `https://app.storno.ro/api`), NOT a separate subdomain. | | `CORS_ALLOW_ORIGIN` | No | localhost | CORS allowed origins regex. Must match your `FRONTEND_URL`. Example: `^https://app\.storno\.ro$` | ## Authentication | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `JWT_PASSPHRASE` | Yes | — | Passphrase for JWT RSA key pair | | `REGISTRATION_ENABLED` | No | `1` | Set to `0` to disable public registration | | `GOOGLE_CLIENT_ID` | No | — | Google OAuth client ID for Google Sign-In. In Docker Compose, this single value is mapped to `OAUTH_GOOGLE_CLIENT_ID` (backend) and `NUXT_PUBLIC_GOOGLE_CLIENT_ID` (frontend) automatically. | | `GOOGLE_CLIENT_SECRET` | No | — | Google OAuth client secret. Mapped to `OAUTH_GOOGLE_CLIENT_SECRET` (backend) in Docker Compose. | | `TURNSTILE_SECRET_KEY` | No | — | Cloudflare Turnstile secret key for bot protection on login/register. If empty, captcha validation is skipped. | | `TURNSTILE_SITE_KEY` | No | — | Cloudflare Turnstile site key. If empty, a test key is used (always passes). Get keys from [Cloudflare dashboard](https://dash.cloudflare.com/?to=/:account/turnstile). | ## ANAF / e-Factura | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `OAUTH_ANAF_CLIENT_ID` | No | — | ANAF OAuth client ID for e-Factura API access. Register at [ANAF API portal](https://www.anaf.ro/anaf/internet/ANAF/servicii_online/inreg_api). | | `OAUTH_ANAF_CLIENT_SECRET` | No | — | ANAF OAuth client secret | | `OAUTH_ANAF_CLIENT_REDIRECT_URI` | No | — | OAuth callback URL registered with ANAF. Must be `https:///auth/callback/anaf/` (e.g. `https://app.storno.ro/auth/callback/anaf/`). | | `REDIRECT_AFTER_OAUTH` | No | — | Frontend URL to redirect to after ANAF OAuth flow | ## Email | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `MAILER_DSN` | Yes | — | Mail transport DSN, e.g. `ses+smtp://KEY:SECRET@email.eu-west-1.amazonaws.com` | | `MAIL_FROM` | No | `noreply@storno.ro` | Default sender email address | ## Storage (S3) | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `AWS_S3_BUCKET` | Yes | — | S3 bucket name for file storage (PDFs, XMLs, attachments) | | `AWS_DEFAULT_REGION` | No | `us-east-1` | AWS region for S3 | | `AWS_ACCESS_KEY_ID` | Yes | — | AWS IAM access key | | `AWS_SECRET_ACCESS_KEY` | Yes | — | AWS IAM secret key | | `STORAGE_ENCRYPTION_KEY` | No | — | Encryption key for user-provided storage credentials | ## Queue & Cache | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `REDIS_URL` | No | `redis://localhost:6379` | Redis connection URL. Used for cache and message queue in production. | In development, the message queue and cache use the filesystem/database automatically. Redis is only required in production. ## Real-time (Centrifugo) | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `CENTRIFUGO_API_URL` | No | — | Centrifugo HTTP API URL, e.g. `http://centrifugo:8000/api` | | `CENTRIFUGO_API_KEY` | No | — | Centrifugo API key for server-to-server calls | | `CENTRIFUGO_TOKEN_HMAC_SECRET` | No | — | HMAC secret for generating client connection tokens | ## Stripe (Payments) | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `STRIPE_SECRET_KEY` | No | — | Stripe API secret key | | `STRIPE_PUBLISHABLE_KEY` | No | — | Stripe publishable key (exposed to frontend) | | `STRIPE_WEBHOOK_SECRET` | No | — | Signing secret for Stripe webhook verification | | `STRIPE_CONNECT_WEBHOOK_SECRET` | No | — | Signing secret for Stripe Connect webhooks | | `STRIPE_PLATFORM_FEE_PERCENT` | No | `2.0` | Platform fee percentage for Stripe Connect payments | ## PDF & Validation | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `WKHTMLTOPDF_PATH` | No | `/usr/local/bin/wkhtmltopdf` | Path to wkhtmltopdf binary for PDF generation | | `JAVA_SERVICE_URL` | No | `http://127.0.0.1:8082` | Java service URL for UBL XML validation and digital signatures | ## Self-Hosted | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `LICENSE_KEY` | Yes | — | License key for self-hosted instances. Obtain from [Licensing](/concepts/licensing). | | `LICENSE_SERVER_URL` | No | `https://api.storno.ro` | License validation server URL | --- ## Minimal Configuration For a minimal self-hosted deployment, these variables are required: ```bash APP_SECRET=your-random-secret-string-here APP_ENV=prod DATABASE_URL=mysql://storno:password@db:3306/storno JWT_PASSPHRASE=your-jwt-passphrase FRONTEND_URL=https://your-domain.com MAILER_DSN=smtp://user:pass@smtp.example.com:587 AWS_S3_BUCKET=your-bucket AWS_ACCESS_KEY_ID=AKIA... AWS_SECRET_ACCESS_KEY=your-secret LICENSE_KEY=your-license-key ``` Generate the JWT key pair after setting the passphrase: ```bash php bin/console lexik:jwt:generate-keypair ``` --- ## Error Handling > Understand error response formats and HTTP status codes used by the Storno.ro API. URL: https://docs.storno.ro/getting-started/errors # Error Handling The Storno.ro API uses standard HTTP status codes and returns structured error responses in JSON format. ## Error Response Format All error responses follow this structure: ```json { "error": "Short error description", "message": "Detailed human-readable explanation", "code": 400 } ``` ### Validation Errors When a request fails validation, the response includes field-level error details: ```json { "error": "Validation failed", "message": "The request contains invalid data", "code": 400, "errors": { "issueDate": "This value should not be blank.", "lines": "At least one line item is required.", "lines[0].quantity": "This value should be greater than 0." } } ``` ## HTTP Status Codes ### Success Codes | Code | Description | |------|-------------| | 200 | Request succeeded | | 201 | Resource created successfully | | 204 | Request succeeded with no response body (e.g., DELETE) | ### Client Error Codes | Code | Description | |------|-------------| | 400 | **Bad Request** — Invalid request body, missing required fields, or validation errors | | 401 | **Unauthorized** — Missing, invalid, or expired JWT token | | 402 | **Payment Required** — Feature not available on current plan (see `PLAN_LIMIT` code) | | 403 | **Forbidden** — Valid token but insufficient permissions (e.g., wrong company, role restriction) | | 404 | **Not Found** — Resource doesn't exist or belongs to a different company | | 405 | **Method Not Allowed** — HTTP method not supported for this endpoint | | 409 | **Conflict** — Resource state conflict (e.g., issuing an already-issued invoice, duplicate IBAN) | | 422 | **Unprocessable Entity** — Request is well-formed but semantically invalid | | 429 | **Too Many Requests** — Rate limit exceeded | ### Server Error Codes | Code | Description | |------|-------------| | 500 | **Internal Server Error** — Unexpected server error | | 502 | **Bad Gateway** — ANAF service unreachable | | 503 | **Service Unavailable** — Server temporarily unavailable | ## Common Error Scenarios ### Authentication Errors ```json // 401 — Expired token { "code": 401, "message": "Expired JWT Token" } // 401 — Invalid credentials { "code": 401, "message": "Invalid credentials." } ``` ### Company Context Errors ```json // 403 — Missing X-Company header { "error": "Company context required", "message": "The X-Company header is required for this endpoint.", "code": 403 } // 403 — No access to company { "error": "Access denied", "message": "You do not have access to this company.", "code": 403 } ``` ### Plan Limit Errors ```json // 402 — Feature not available on current plan { "error": "Recurring invoices are not available on your plan.", "code": "PLAN_LIMIT" } // 402 — Monthly invoice limit reached { "error": "Monthly invoice limit reached.", "code": "PLAN_LIMIT", "limit": 100 } ``` Plan limit errors apply equally to web, mobile, and API key authenticated requests. See [Plans & Features](/concepts/licensing#plans--features) for details on what each plan includes. ### Resource State Errors ```json // 409 — Invoice already issued { "error": "Invalid state transition", "message": "Cannot issue an invoice that is already issued.", "code": 409 } // 409 — Cannot delete issued invoice { "error": "Cannot delete", "message": "Cannot delete an issued invoice. Cancel it first.", "code": 409 } ``` ## Handling Errors ### Retry Strategy - **401**: Refresh your token and retry the request - **429**: Wait for the duration specified in the `Retry-After` header - **500/502/503**: Retry with exponential backoff (max 3 retries) - **402**: Upgrade your plan — see [Plans & Features](/concepts/licensing#plans--features) - **400/403/404/409**: Do not retry — fix the request ### Example Error Handler ```js async function apiRequest(url, options = {}) { const response = await fetch(url, { ...options, headers: { 'Authorization': `Bearer ${token}`, 'X-Company': companyUuid, 'Content-Type': 'application/json', ...options.headers, }, }); if (!response.ok) { const error = await response.json(); if (response.status === 401) { // Token expired — refresh and retry await refreshToken(); return apiRequest(url, options); } throw new ApiError(error.message, response.status, error.errors); } return response.json(); } ``` --- ## Pagination > How to paginate through list endpoints in the Storno.ro API. URL: https://docs.storno.ro/getting-started/pagination # Pagination All list endpoints support pagination using `page` and `limit` query parameters. ## Request Parameters | Parameter | Type | Default | Description | |-----------|--------|---------|-------------| | page | number | 1 | Page number (1-indexed) | | limit | number | 20 | Items per page (max varies by endpoint, typically 100) | ## Example Request ```bash curl "https://api.storno.ro/api/v1/invoices?page=2&limit=25" \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" ``` ## Response Format Paginated responses include metadata alongside the data: ```json { "data": [ { "uuid": "...", "number": "FACT-001", ... }, { "uuid": "...", "number": "FACT-002", ... } ], "total": 150, "page": 2, "limit": 25, "pages": 6 } ``` | Field | Type | Description | |-------|--------|-------------| | data | array | Array of resource objects | | total | number | Total number of matching resources | | page | number | Current page number | | limit | number | Items per page | | pages | number | Total number of pages | ## Filtering and Sorting Many list endpoints support additional query parameters for filtering and sorting. Common filters include: | Parameter | Type | Description | |-----------|--------|-------------| | search | string | Full-text search across relevant fields | | status | string | Filter by resource status | | from | string | Filter by start date (YYYY-MM-DD) | | to | string | Filter by end date (YYYY-MM-DD) | | sort | string | Sort field (e.g., `issueDate`, `number`) | | order | string | Sort direction: `asc` or `desc` | See individual endpoint documentation for available filters. ## Grouped Responses Some endpoints (e.g., clients, suppliers) return grouped results instead of paginated arrays: ```json { "data": { "A": [ { "uuid": "...", "name": "ABC Corp" } ], "S": [ { "uuid": "...", "name": "SC Firma SRL" } ] }, "total": 45 } ``` These endpoints group results alphabetically by the first letter of the name. ## Best Practices - Use reasonable `limit` values (20–50) for UI pagination - Use larger limits (100) for bulk data exports - Always check `pages` to know when you've reached the end - Cache `total` to avoid re-fetching it on every page change --- ## Quickstart > Make your first API call to the Storno.ro API in under 5 minutes. URL: https://docs.storno.ro/getting-started/quickstart # Quickstart This guide walks you through making your first API call — from authentication to creating an invoice. ## Prerequisites - An Storno.ro account ([register here](https://app.storno.ro/register)) - At least one company added to your account - A valid e-invoice provider token (e.g., ANAF for Romania) if you plan to submit electronically ## Step 1: Authenticate ```bash curl -X POST https://api.storno.ro/api/auth \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "password": "your_password" }' ``` Save the `token` from the response. You'll use it in all subsequent requests. ## Step 2: List Your Companies ```bash curl https://api.storno.ro/api/v1/companies \ -H "Authorization: Bearer {token}" ``` **Response:** ```json [ { "uuid": "550e8400-e29b-41d4-a716-446655440000", "name": "SC Firma Mea SRL", "cif": "RO12345678", "syncEnabled": true } ] ``` Save the company `uuid` — you'll use it as the `X-Company` header. ## Step 3: Get Invoice Defaults Before creating an invoice, fetch the available options (VAT rates, currencies, payment methods, etc.): ```bash curl https://api.storno.ro/api/v1/invoice-defaults \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" ``` This returns VAT rates, currencies, units of measure, payment terms, and payment methods configured for your company. ## Step 4: Create an Invoice ```bash curl -X POST https://api.storno.ro/api/v1/invoices \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" \ -H "Content-Type: application/json" \ -d '{ "clientId": "{client_uuid}", "seriesId": "{series_uuid}", "issueDate": "2026-01-15", "dueDate": "2026-02-15", "currency": "RON", "lines": [ { "name": "Servicii consultanță IT", "quantity": 10, "unitPrice": 500, "vatRateId": "{vat_rate_uuid}", "unit": "ore" } ] }' ``` **Response:** ```json { "uuid": "7a1b2c3d-4e5f-6789-abcd-ef0123456789", "number": "FACT-001", "status": "draft", "totalWithoutVat": 5000, "totalVat": 950, "totalWithVat": 5950, "currency": "RON" } ``` ## Step 5: Issue the Invoice Issuing validates the invoice, generates the XML, and optionally schedules e-invoice submission: ```bash curl -X POST https://api.storno.ro/api/v1/invoices/{uuid}/issue \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" ``` ## Step 6: Check Invoice Status ```bash curl https://api.storno.ro/api/v1/invoices/{uuid} \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" ``` The invoice `status` will progress through: `draft` → `issued` → `sent` → `delivered` (or `error`). ## Next Steps - [Authentication](/getting-started/authentication) — Learn about token refresh and OAuth - [Error Handling](/getting-started/errors) — Understand error responses - [Multi-Tenancy](/concepts/multi-tenancy) — How organizations and companies work - [Invoice Object](/objects/invoice) — Full invoice field reference --- ## Rate Limiting > Understand API rate limits, throttling policies, and how to handle 429 responses. URL: https://docs.storno.ro/getting-started/rate-limiting # Rate Limiting All API requests are subject to rate limiting to protect the platform and ensure fair usage. Limits vary by endpoint type and authentication status. ## General API Limits | Tier | Bucket Size | Refill Rate | Applied Per | |------|-------------|-------------|-------------| | Unauthenticated | 10,000 requests | 100 per 5 minutes | IP address | | Authenticated | 50,000 requests | 500 per 2 minutes | IP address | General API limits use a **token bucket** policy — each request consumes one token. Tokens refill at a steady rate. When the bucket is empty, requests are rejected with `429 Too Many Requests`. ## Authentication Endpoint Limits Authentication endpoints use stricter **sliding window** limits to prevent brute-force attacks: | Endpoint | Limit | Window | Applied Per | |----------|-------|--------|-------------| | `POST /api/auth` (login) | 5 attempts | 1 minute | IP address | | `POST /api/auth/register` | 10 attempts | 1 hour | IP address | | `POST /api/auth/mfa/verify` | 5 attempts | 1 minute | IP address | | `POST /api/auth/google` | 10 attempts | 1 minute | IP address | | `POST /api/auth/passkey/login` | 10 attempts | 1 minute | IP address | | `POST /api/auth/passkey/login/options` | 10 attempts | 1 minute | IP address | | `POST /api/auth/forgot-password` | 3 attempts | 15 minutes | IP address | | `POST /api/auth/reset-password` | 3 attempts | 15 minutes | IP address | ## Other Endpoint Limits | Endpoint | Limit | Window | Applied Per | |----------|-------|--------|-------------| | `POST /api/v1/contact` | 3 submissions | 1 hour | IP address | | `POST /api/v1/licensing/validate` | 10 attempts | 1 hour | IP address | ## ANAF Integration Limits ANAF (Romanian Tax Authority) endpoints have dedicated rate limiters to comply with ANAF API restrictions: | Operation | Limit | Window | Applied Per | |-----------|-------|--------|-------------| | All ANAF calls (global) | 1,000 requests | 1 minute | System-wide | | List messages | 1,500 requests | 1 day | Company CIF | | Download message | 10 requests | 1 day | Message ID | | Check upload status | 100 requests | 1 day | Upload ID | | Upload response | 1,000 requests | 1 day | Company CIF | ## Handling Rate Limit Responses When a rate limit is exceeded, the API returns a `429 Too Many Requests` response: ```json { "error": "Too many requests. Please try again later." } ``` For ANAF endpoints, the response includes a `Retry-After` header and value: ```json { "error": "ANAF rate limit reached. Try again later.", "retryAfter": 45 } ``` ### Best Practices - **Implement exponential backoff** — when you receive a `429`, wait before retrying. Double the wait time on each consecutive failure. - **Cache responses** — avoid unnecessary duplicate requests, especially for data that doesn't change frequently (exchange rates, company details). - **Use webhooks** — instead of polling for status changes, subscribe to [webhook events](/concepts/webhooks-events) for real-time updates. - **Batch operations** — where the API supports it, use list endpoints with filters instead of making individual requests. ### Example: Retry with Backoff ```js async function fetchWithRetry(url, options, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, options); if (response.status !== 429) return response; const retryAfter = response.headers.get('Retry-After'); const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000; await new Promise(resolve => setTimeout(resolve, waitMs)); } throw new Error('Rate limit exceeded after retries'); } ``` ```php function fetchWithRetry(string $url, array $options, int $maxRetries = 3): Response { for ($attempt = 0; $attempt < $maxRetries; $attempt++) { $response = $client->request('GET', $url, $options); if ($response->getStatusCode() !== 429) { return $response; } $retryAfter = $response->getHeaderLine('Retry-After'); $waitSeconds = $retryAfter ? (int) $retryAfter : pow(2, $attempt); sleep($waitSeconds); } throw new \RuntimeException('Rate limit exceeded after retries'); } ``` --- ## Security > Security practices, vulnerability reporting, and data protection in Storno.ro. URL: https://docs.storno.ro/getting-started/security # Security Storno.ro handles sensitive financial data. This page describes our security practices and how to report vulnerabilities. --- ## Reporting Vulnerabilities If you discover a security vulnerability, please report it responsibly: - **Email**: [security@storno.ro](mailto:security@storno.ro) - **Do not** open a public GitHub issue for security vulnerabilities - Include a clear description of the vulnerability and steps to reproduce - We will acknowledge receipt within 48 hours - We aim to release a fix within 7 days for critical issues We appreciate responsible disclosure and will credit researchers who report valid vulnerabilities (unless they prefer to remain anonymous). --- ## Authentication & Access Control ### Authentication Methods | Method | Second Factor Required | |--------|----------------------| | Email + Password | Yes (if MFA enabled) | | Google OAuth | Yes (if MFA enabled) | | Passkeys (WebAuthn) | No (inherently multi-factor) | | API Keys | No (long-lived, scoped tokens) | ### Multi-Factor Authentication (MFA) - TOTP-based 2FA (Google Authenticator, Authy, etc.) - 10 single-use backup codes generated on setup - MFA challenge tokens expire after 5 minutes - Maximum 5 verification attempts per challenge - See [Authentication](/getting-started/authentication) for details ### JWT Tokens - RSA-signed (RS256) access tokens - Access tokens expire after 1 hour (configurable) - Refresh tokens expire after 30 days (configurable) - Refresh tokens are rotated on each use - Tokens are invalidated on password change ### API Keys - Scoped permissions (read-only, write, admin) - Stored as hashed values — the full key is shown only once at creation - Can be revoked instantly - No MFA bypass — API keys are for programmatic access only ### Passkeys (WebAuthn) - FIDO2/WebAuthn standard - Public key cryptography — no shared secrets - Phishing-resistant by design - Biometric or PIN verification handled by the device --- ## Data Protection ### Encryption | Layer | Method | |-------|--------| | In transit | TLS 1.2+ (HTTPS required for all API calls) | | Database passwords | bcrypt (PASSWORD_DEFAULT) | | MFA backup codes | bcrypt hashed | | TOTP secrets | Stored in database (equivalent to a password, useless without current time window) | | API keys | SHA-256 hashed | | User storage credentials | AES-256-CBC with per-instance encryption key | ### Data Isolation - **Multi-tenant architecture**: Organizations are fully isolated at the database level - **Company context**: API requests require an `X-Company` header; cross-company access is denied - **Role-based access control (RBAC)**: 40+ granular permissions across Owner, Admin, Accountant, and Viewer roles - **Soft deletion**: Deleted records are retained for audit trails but excluded from API responses --- ## Rate Limiting All endpoints are rate-limited to prevent abuse. Authentication endpoints have stricter limits: - Login: 5 attempts per minute - Registration: 10 attempts per hour - MFA verification: 5 attempts per minute - Password reset: 3 attempts per 15 minutes See [Rate Limiting](/getting-started/rate-limiting) for the complete reference. --- ## Self-Hosted Security ### License Validation - License keys are signed JWTs validated entirely offline via RSA signature verification - No network calls are made to the SaaS server — no data is transmitted - Works fully offline, air-gapped, or behind a firewall ### Recommendations - **Keep Docker images updated** — pull the latest images regularly for security patches - **Use a reverse proxy with SSL** — never expose the backend directly to the internet - **Set strong secrets** — use `openssl rand -hex 32` for `APP_SECRET`, `JWT_PASSPHRASE`, and Centrifugo keys - **Restrict database access** — MySQL and Redis should not be accessible from the public internet - **Enable MFA** — require two-factor authentication for all admin users - **Rotate API keys** — periodically regenerate API keys, especially after team member departures - **Monitor logs** — watch for unusual patterns in authentication failures --- ## Compliance - Storno.ro is designed for multi-country e-invoicing and complies with provider requirements (ANAF, XRechnung, SDI, KSeF, Factur-X) - XML documents are digitally signed and validated against provider-specific schemas - All financial data can be exported for audit purposes - GDPR: Users can request account deletion, which permanently removes all personal data --- ## Self-Hosting > Deploy Storno.ro on your own infrastructure using Docker. URL: https://docs.storno.ro/getting-started/self-hosting # Self-Hosting Storno.ro can be deployed on your own servers using Docker. Self-hosted instances connect to the Storno.ro SaaS for license validation and plan management, while all your data stays on your infrastructure. ## Prerequisites - Docker and Docker Compose installed - A valid license key from [app.storno.ro/settings/billing](https://app.storno.ro/settings/billing) - A domain name with SSL (recommended for production) ## Quick Start ### 1. Download the deployment files ```bash mkdir storno && cd storno curl -O https://raw.githubusercontent.com/stornoro/storno/main/deploy/docker-compose.yml curl -O https://raw.githubusercontent.com/stornoro/storno/main/deploy/.env.example curl -O https://raw.githubusercontent.com/stornoro/storno/main/deploy/centrifugo.json ``` ### 2. Configure environment ```bash cp .env.example .env ``` Edit `.env` and fill in the required values: ```bash # Generate secrets (run each one separately) openssl rand -hex 32 # → APP_SECRET openssl rand -hex 32 # → JWT_PASSPHRASE openssl rand -hex 32 # → CENTRIFUGO_API_KEY openssl rand -hex 32 # → CENTRIFUGO_TOKEN_HMAC_SECRET # Set your database password MYSQL_ROOT_PASSWORD=your-strong-password MYSQL_PASSWORD=your-strong-password # Paste your license key LICENSE_KEY=your-license-key-here ``` ### 3. Start the services ```bash docker compose --profile local-db up -d ``` This starts five containers: | Service | Description | Default Port | |---------|-------------|-------------| | `backend` | PHP API server (Symfony + Nginx) | 8900 | | `frontend` | Nuxt SSR web application | 8901 | | `db` | MySQL 8.0 database | 3306 | | `redis` | Redis 7 (cache, queues, locks) | 6379 | | `centrifugo` | WebSocket server for real-time updates | 8445 | ### 4. Initialize the database On first startup, create the database schema and mark all migrations as applied: ```bash docker compose exec backend php bin/console doctrine:schema:create docker compose exec backend php bin/console doctrine:migrations:sync-metadata-storage docker compose exec backend php bin/console doctrine:migrations:version --add --all --no-interaction ``` ### 5. Create the first user ```bash docker compose exec backend php bin/console app:user:create \ --email=admin@yourdomain.com \ --password=your-password \ --admin ``` JWT keys are generated automatically on first startup — no manual step needed. ### 6. Access the application Open `http://localhost:8901` in your browser (or your configured domain) and log in. --- ## Environment Variables ### Required | Variable | Description | Example | |----------|-------------|---------| | `APP_SECRET` | Symfony application secret | `openssl rand -hex 32` | | `JWT_PASSPHRASE` | JWT key passphrase | `openssl rand -hex 32` | | `MYSQL_ROOT_PASSWORD` | MySQL root password | — | | `MYSQL_PASSWORD` | MySQL user password | — | | `CENTRIFUGO_API_KEY` | Centrifugo internal API key | `openssl rand -hex 32` | | `CENTRIFUGO_TOKEN_HMAC_SECRET` | Centrifugo HMAC secret | `openssl rand -hex 32` | | `LICENSE_KEY` | Your Storno.ro license key | Get from SaaS dashboard | ### Optional | Variable | Default | Description | |----------|---------|-------------| | `BACKEND_PORT` | `8900` | Backend API port | | `FRONTEND_PORT` | `8901` | Frontend web port | | `CENTRIFUGO_PORT` | `8445` | WebSocket port | | `MYSQL_PORT` | `3306` | MySQL port | | `MYSQL_DATABASE` | `storno` | Database name | | `MYSQL_USER` | `storno` | Database user | | `FRONTEND_URL` | `http://localhost:8901` | Public URL for the frontend | | `PUBLIC_API_BASE` | `/api` | How the browser reaches the API | | `CORS_ALLOW_ORIGIN` | `localhost` pattern | CORS allowed origins regex | | `MAILER_DSN` | `null://null` | SMTP/SES transport DSN | | `MAIL_FROM` | `noreply@storno.ro` | Sender email address | | `GOOGLE_CLIENT_ID` | — | Google OAuth client ID (optional). Mapped to both backend and frontend containers. | | `GOOGLE_CLIENT_SECRET` | — | Google OAuth client secret (optional). Mapped to the backend container. | | `AWS_S3_BUCKET` | — | S3 bucket name (local disk used if AWS credentials are not set) | | `AWS_DEFAULT_REGION` | `us-east-1` | AWS region | | `AWS_ACCESS_KEY_ID` | — | AWS access key (set to enable S3 storage) | | `AWS_SECRET_ACCESS_KEY` | — | AWS secret key | | `LICENSE_SERVER_URL` | `https://app.storno.ro` | License validation server (do not change) | --- ## License Key Your license key connects your self-hosted instance to your Storno.ro subscription. The key is validated periodically against the SaaS server. ### Obtaining a License Key 1. Log in to [app.storno.ro](https://app.storno.ro) 2. Go to **Settings → Billing** 3. Go to **Settings → Licensing** and generate a new key 5. Copy the key and paste it in your `.env` file as `LICENSE_KEY` ### How Validation Works - The license key is a signed JWT validated **entirely offline** — no network calls to the SaaS server - Plan, features, and expiration are embedded in the JWT and verified via RSA signature - When the key expires, the instance falls back to the Community (free) plan - **No user data or business information is ever transmitted** ### License Sync Command The license is validated automatically every 6 hours, but you can also run it manually: ```bash docker compose exec backend php bin/console app:license:sync ``` **After changing `LICENSE_KEY` in your `.env`**, you must restart the backend and run the sync command for the new key to take effect: ```bash docker compose restart backend docker compose exec -T backend php bin/console app:license:sync ``` --- ## Reverse Proxy Setup For production, place a reverse proxy (Nginx, Caddy, Traefik) in front of the services to handle SSL termination. ### Nginx Example **Step 1:** Create an HTTP-only config at `/etc/nginx/sites-available/storno.conf`: ```nginx server { listen 80; server_name app.storno.ro; # Frontend location / { proxy_pass http://127.0.0.1:8901; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # API location /api { proxy_pass http://127.0.0.1:8900; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 50M; } # WebSocket location /connection/websocket { proxy_pass http://127.0.0.1:8445; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; } } ``` Enable the site: ```bash ln -s /etc/nginx/sites-available/storno.conf /etc/nginx/sites-enabled/ nginx -t && systemctl reload nginx ``` **Step 2:** Install SSL with Certbot (Let's Encrypt). Certbot will automatically modify the nginx config to add HTTPS (port 443) and redirect HTTP to HTTPS: ```bash # Install certbot (Ubuntu/Debian) apt install certbot python3-certbot-nginx # Obtain certificate and auto-configure nginx for SSL certbot --nginx -d app.storno.ro # Verify auto-renewal certbot renew --dry-run ``` **Step 3:** Update your `.env` to match: ```bash FRONTEND_URL=https://app.storno.ro PUBLIC_API_BASE=https://app.storno.ro/api CORS_ALLOW_ORIGIN=^https://app\.storno\.ro$ CENTRIFUGO_ALLOWED_ORIGINS=https://app.storno.ro ``` `PUBLIC_API_BASE` is your `FRONTEND_URL` + `/api` — it is NOT a separate subdomain. The nginx config above proxies `/api` requests to the backend container. --- ## Upgrading To upgrade to the latest version: ```bash make update ``` Or manually: ```bash docker compose pull docker compose up -d docker compose exec backend php bin/console doctrine:migrations:migrate --no-interaction docker compose exec backend php bin/console cache:clear ``` --- ## Backups ### Database ```bash docker compose exec db mysqldump -u root -p storno > backup_$(date +%Y%m%d).sql ``` ### Application Data Back up the Docker volumes for persistent data: ```bash # List volumes docker volume ls | grep storno # Backup database volume docker run --rm -v storno_db_data:/data -v $(pwd):/backup alpine tar czf /backup/db_data.tar.gz /data # Backup uploaded documents docker run --rm -v storno_backend_var:/data -v $(pwd):/backup alpine tar czf /backup/backend_var.tar.gz /data ``` ### Company-Level Backup Storno.ro also supports per-company backup/restore through the API: ```bash # Export company data curl -X POST https://your-instance/api/v1/backup/export \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" # Import company data curl -X POST https://your-instance/api/v1/backup/import \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" \ -F "file=@backup.zip" ``` --- ## Troubleshooting ### License validation fails ```bash # Check license status docker compose exec backend php bin/console app:license:sync # Verify LICENSE_KEY is set docker compose exec backend printenv LICENSE_KEY # Test connectivity to SaaS docker compose exec backend curl -s https://app.storno.ro/api/health ``` ### Database connection refused ```bash # Check if MySQL is healthy docker compose ps db # View MySQL logs docker compose logs db ``` ### WebSocket not connecting Both the frontend and mobile app derive the WebSocket URL automatically from the current host (e.g. `https://factura.yourdomain.com` → `wss://factura.yourdomain.com/connection/websocket`). No extra configuration is needed. 1. **Ensure your reverse proxy** forwards WebSocket upgrades on `/connection/websocket` to the Centrifugo container (see the Nginx/Caddy examples above). 2. **Check Centrifugo** is running: ```bash docker compose logs centrifugo ``` ### View application logs ```bash # All services docker compose logs -f # Specific service docker compose logs -f backend docker compose logs -f frontend ``` --- ## System Requirements > Minimum and recommended hardware and software requirements for self-hosted Storno.ro deployments. URL: https://docs.storno.ro/getting-started/system-requirements # System Requirements Hardware and software requirements for running Storno.ro on your own infrastructure. ## Software Requirements ### Docker Deployment (Recommended) | Component | Version | |-----------|---------| | Docker | 24.0+ | | Docker Compose | 2.20+ | All other dependencies are included in the Docker images. ### Manual Deployment | Component | Version | Notes | |-----------|---------|-------| | PHP | 8.2+ | FPM recommended | | MySQL | 8.0+ | `utf8mb4` charset | | Redis | 7.0+ | Cache, queues, and rate limiting | | Node.js | 20+ | Frontend SSR | | Nginx | 1.24+ | Reverse proxy | | Java JRE | 17+ | UBL XML validation and digital signatures | | Centrifugo | 5.x | Real-time WebSocket server | ### Required PHP Extensions `pdo_mysql`, `intl`, `opcache`, `zip`, `gd` (with freetype + JPEG), `mbstring`, `bcmath`, `sockets`, `redis`, `apcu`, `ctype`, `iconv` --- ## Hardware Requirements ### Minimum (Small Team, < 5 users) | Resource | Specification | |----------|---------------| | CPU | 2 vCPUs | | RAM | 4 GB | | Disk | 20 GB SSD | | Network | 10 Mbps | Suitable for small businesses with up to 500 invoices/month. ### Recommended (Medium Team, 5–25 users) | Resource | Specification | |----------|---------------| | CPU | 4 vCPUs | | RAM | 8 GB | | Disk | 50 GB SSD | | Network | 100 Mbps | Suitable for businesses with multiple companies and thousands of invoices/month. ### Production (Large Team, 25+ users) | Resource | Specification | |----------|---------------| | CPU | 8+ vCPUs | | RAM | 16+ GB | | Disk | 100+ GB SSD | | Network | 100+ Mbps | For high-volume deployments. Consider separating the database onto its own server. --- ## Resource Allocation (Kubernetes) If deploying with Helm/Kubernetes, the recommended resource limits per pod: | Pod | CPU Request | CPU Limit | Memory Request | Memory Limit | Storage | |-----|-------------|-----------|----------------|--------------|---------| | Backend (PHP) | 250m | 1000m | 256 Mi | 1 Gi | 12 Gi | | Frontend (Node) | 100m | 500m | 128 Mi | 512 Mi | — | | MySQL | 250m | 1000m | 512 Mi | 2 Gi | 10 Gi | | Redis | 50m | 200m | 64 Mi | 256 Mi | 2 Gi | | Centrifugo | 50m | 500m | 64 Mi | 256 Mi | 1 Gi | --- ## Disk Space Considerations - **Database**: Grows with invoice volume. ~1 GB per 50,000 invoices (including line items, payments, audit logs). - **File storage**: PDF and XML files are stored on disk or S3. ~50 KB per invoice (PDF + XML). 100,000 invoices ≈ 5 GB. - **Logs**: Application and Nginx logs. Configure log rotation to prevent disk exhaustion. - **Backups**: Plan for at least 2x your data size if storing backups locally. For production deployments, use S3-compatible object storage for files instead of local disk. This simplifies backups and allows horizontal scaling. --- ## Network Requirements | Service | Port | Protocol | Direction | |---------|------|----------|-----------| | HTTPS | 443 | TCP | Inbound | | MySQL | 3306 | TCP | Internal only | | Redis | 6379 | TCP | Internal only | | Centrifugo WS | 8444 | TCP | Internal (proxied via 443) | | SMTP | 587 | TCP | Outbound | | ANAF API | 443 | TCP | Outbound to `api.anaf.ro` | | License validation | 443 | TCP | Outbound to `api.storno.ro` | | S3 storage | 443 | TCP | Outbound to your S3 provider | The backend must have outbound HTTPS access to `api.anaf.ro` for e-Factura integration and to `api.storno.ro` for license validation. --- ## Supported Platforms Storno.ro Docker images are built for `linux/amd64`. Tested on: - Ubuntu 22.04 / 24.04 - Debian 12 - Amazon Linux 2023 - Alpine Linux (container-native) Cloud providers: AWS (EC2, ECS, EKS), Google Cloud (GCE, GKE), Azure (VM, AKS), DigitalOcean, Hetzner, OVH. --- ## Telemetry > What telemetry data Storno.ro collects, how it is used, and how to opt out. URL: https://docs.storno.ro/getting-started/telemetry # Telemetry Storno.ro collects anonymous usage telemetry from mobile clients to improve the product. Telemetry is **opt-out** and **never includes** customer financial data, invoice contents, client details, or any personally identifiable information (PII). --- ## What Data Is Collected Each telemetry event contains: | Field | Description | Example | |-------|-------------|---------| | `event` | Action name (dot-separated) | `invoice.created` | | `properties` | Optional metadata about the action | `{ "method": "email" }`, `{ "refund": true }` | | `platform` | Client platform identifier | `mobile`, `web` | | `app_version` | App version string | `1.4.2` | | `timestamp` | ISO 8601 timestamp of the event | `2026-03-05T14:30:00Z` | | `user_id` | Internal user UUID | — | | `company_id` | Active company UUID (from `X-Company` header) | — | ### Events Tracked **Invoicing** | Event | Properties | Triggered When | |-------|-----------|---------------| | `invoice.created` | `refund` (boolean) | A new invoice is created | | `invoice.issued` | — | An invoice is finalized | | `invoice.sent_email` | — | An invoice is emailed to a client | | `invoice.payment_recorded` | — | A payment is recorded on an invoice | | `invoice.exported_pdf` | — | An invoice PDF is downloaded | | `invoice.deleted` | — | An invoice is deleted | **Clients** | Event | Properties | Triggered When | |-------|-----------|---------------| | `client.created` | `fromRegistry` (boolean) | A new client is created | | `client.updated` | — | A client is updated | | `client.deleted` | — | A client is deleted | **e-Factura** | Event | Properties | Triggered When | |-------|-----------|---------------| | `efactura.submitted` | — | An invoice is submitted to ANAF | | `efactura.token_connected` | — | ANAF OAuth token is connected | | `efactura.sync_triggered` | — | e-Factura sync is manually triggered | **Documents** | Event | Properties | Triggered When | |-------|-----------|---------------| | `document.proforma_created` | — | A proforma invoice is created | | `document.receipt_created` | — | A receipt is created | | `document.delivery_note_created` | — | A delivery note is created | **Account** | Event | Properties | Triggered When | |-------|-----------|---------------| | `account.logged_in` | `method` (`email`, `google`, `passkey`) | User logs in | | `account.registered` | — | New account is created | | `account.company_switched` | — | User switches active company | | `account.language_changed` | — | User changes language | | `account.push_enabled` | — | Push notifications are enabled | --- ## What Is NOT Collected - Invoice amounts, line items, or totals - Client names, addresses, CUI/CIF, or bank accounts - Product names or prices - Email addresses or phone numbers - File contents (PDFs, XMLs) - IP addresses or geolocation - Device identifiers or advertising IDs --- ## How Data Is Sent Events are batched on the client and sent to `POST /api/v1/telemetry` in groups of up to 100 events. The mobile app flushes events every 30 seconds or when the batch reaches 10 events, whichever comes first. Telemetry requests are fire-and-forget — failures are silently ignored and never block the user interface. --- ## How Data Is Used Telemetry helps us: - **Identify popular features** — prioritize improvements for the most-used workflows - **Detect underused features** — investigate whether features need better discoverability or UX - **Monitor adoption** — track platform usage (mobile vs. web) and app version distribution - **Improve reliability** — spot patterns in user workflows that may reveal edge cases Telemetry data is only accessible to Storno.ro platform administrators via the admin dashboard. --- ## Opting Out ### Self-hosted instances Telemetry is collected by the SaaS platform. Self-hosted instances do not send telemetry to Storno.ro — all data stays on your infrastructure. If you run a self-hosted instance and want to disable telemetry collection entirely, set the environment variable: ```bash TELEMETRY_ENABLED=false ``` This disables the `POST /api/v1/telemetry` endpoint. Events sent by clients will be silently discarded. ### SaaS Telemetry collection on the SaaS platform is enabled by default. If you would like your data excluded, contact [support@storno.ro](mailto:support@storno.ro). --- ## ANAF Integration > How Storno.ro integrates with Romania's ANAF e-Factura system for electronic invoicing. URL: https://docs.storno.ro/concepts/anaf-integration # ANAF Integration Storno.ro integrates with Romania's ANAF (Agenția Națională de Administrare Fiscală) e-Factura system for electronic invoice submission and retrieval. ## Overview The e-Factura system requires all B2B invoices in Romania to be submitted electronically in UBL 2.1 XML format. Storno.ro handles: 1. **XML Generation** — Converts invoice data to UBL 2.1 compliant XML 2. **Submission** — Uploads XML to ANAF's SPV (Spațiul Privat Virtual) platform 3. **Status Tracking** — Monitors submission status (validated, rejected, etc.) 4. **Incoming Invoices** — Downloads and parses invoices received from suppliers 5. **Message Monitoring** — Tracks all e-Factura messages from ANAF ## Authentication with ANAF ANAF uses OAuth 2.0 for API authentication. Storno.ro supports two methods for obtaining ANAF tokens: ### Accountant Method (Direct OAuth) The user authenticates directly with ANAF through the OAuth flow: 1. Initiate the OAuth flow: `GET /api/connect/anaf` 2. User logs in on ANAF's website 3. ANAF redirects back with an authorization code 4. Storno.ro exchanges the code for an access token 5. Token is validated against all user's companies ### Device Method (Link-based) For users who can't complete the OAuth flow on the same device (e.g., mobile users): 1. Create a token link: `POST /api/v1/anaf/token-links` 2. Share the link URL with the person who has ANAF access 3. They open the link, complete ANAF OAuth on their device 4. Check link status: `GET /api/v1/anaf/token-links/{linkToken}` 5. Once completed, the token is automatically associated with the user ```bash # Create a token link curl -X POST https://api.storno.ro/api/v1/anaf/token-links \ -H "Authorization: Bearer {token}" # Response { "linkToken": "abc123...", "url": "https://app.storno.ro/anaf/link/abc123...", "expiresAt": "2026-02-17T12:00:00Z" } ``` ### Token Lifecycle - ANAF tokens expire (typically after 90 days) - Storno.ro extracts the JWT expiry and tracks validity - Users are notified before token expiration - Expired tokens must be re-obtained through a new OAuth flow ## Sync Process ### Automatic Sync When sync is enabled for a company (`syncEnabled: true`), Storno.ro periodically: 1. Checks for new outgoing invoice statuses (validated/rejected) 2. Downloads new incoming invoices 3. Parses incoming XML to create invoice, client, and product records 4. Sends notifications for new invoices and status changes ### Manual Sync Trigger a sync manually: ```bash curl -X POST https://api.storno.ro/api/v1/sync/trigger \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" ``` ### Sync Configuration | Setting | Description | Default | |---------|-------------|---------| | `syncEnabled` | Whether automatic sync is active | `false` | | `syncDaysBack` | How many days back to sync on first run | `60` | | `efacturaDelayHours` | Delay before auto-submitting issued invoices | `null` (immediate) | ## Invoice Submission Flow ``` Draft → Issue → (optional delay) → Submit to ANAF → ANAF Processing → Validated/Rejected ``` 1. **Issue** (`POST /invoices/{uuid}/issue`) — Validates data, generates UBL XML, generates PDF 2. **Submit** (`POST /invoices/{uuid}/submit`) — Uploads XML to ANAF 3. **Status Update** — Sync process checks for ANAF response 4. **Validated** — ANAF accepted the invoice; it's now in the national e-Factura system 5. **Rejected** — ANAF found errors; invoice needs correction ### Delayed Submission Companies can configure `efacturaDelayHours` to add a review window between issuing and ANAF submission. This allows catching errors before the invoice enters the national system. ## E-Factura Messages ANAF sends various message types through the SPV platform: ```bash curl https://api.storno.ro/api/v1/efactura-messages \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" ``` Message types include: - **FACTURA_PRIMITA** — Incoming invoice received - **FACTURA_TRIMISA** — Outgoing invoice status update - **ERORI_FACTURA** — Invoice validation errors ## XML Validation Before submission, invoices can be validated against ANAF's rules: ```bash # Quick validation (structural) curl -X POST https://api.storno.ro/api/v1/invoices/{uuid}/validate?mode=quick \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" # Full validation (Schematron rules) curl -X POST https://api.storno.ro/api/v1/invoices/{uuid}/validate?mode=full \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" ``` ## Digital Signature Verification Invoices validated by ANAF receive a digital signature. You can verify it: ```bash curl -X POST https://api.storno.ro/api/v1/invoices/{uuid}/verify-signature \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" ``` --- ## Architecture > System architecture, service topology, and data flow in Storno.ro. URL: https://docs.storno.ro/concepts/architecture # Architecture Storno.ro is a multi-repo platform with several applications and supporting infrastructure services. This page describes how the components fit together. --- ## Service Topology ``` ┌──────────────────────────────┐ │ Nginx (reverse proxy) │ │ app.storno.ro api.storno.ro │ └──────┬───────────────┬────────┘ │ │ ┌──────▼──────┐ ┌─────▼──────┐ │ Frontend │ │ Backend │ │ Nuxt 4 │ │ Symfony 7.4 │ │ :3000 │ │ :8000 │ └──────┬──────┘ └──┬──┬──┬────┘ │ │ │ │ ┌────────────┘ ┌──────┘ │ └──────┐ │ │ │ │ ┌─────▼─────┐ ┌──────▼──┐ ┌───▼───┐ ┌──▼────────┐ │ Mobile │ │ MySQL │ │ Redis │ │ Centrifugo │ │ React │ │ 8.0 │ │ 7 │ │ v5 │ │ Native │ └─────────┘ └───────┘ └────────────┘ └────────────┘ ``` | Service | Repository | Technology | Role | |---------|-----------|-----------|------| | **Backend** | [stornoro/storno](https://github.com/stornoro/storno) | PHP 8.2 + Symfony 7.4 + Nginx | REST API, business logic, async workers | | **Frontend** | [stornoro/storno](https://github.com/stornoro/storno) | Nuxt 4 (Vue 3, SSR) | Web application with server-side rendering | | **Mobile** | [stornoro/storno-mobile-app](https://github.com/stornoro/storno-mobile-app) | React Native + Expo | iOS and Android mobile app | | **Docs** | [stornoro/docs](https://github.com/stornoro/docs) | Next.js + Markdoc | API documentation | | **CLI** | [stornoro/storno-cli](https://github.com/stornoro/storno-cli) | TypeScript (MCP) | CLI tool for AI assistants | | **MySQL** | — | 8.0 | Primary database (57 entities) | | **Redis** | — | 7.x | Cache, message queue, rate limiting, locks | | **Centrifugo** | — | v5 | WebSocket server for real-time updates | The backend container bundles PHP-FPM, Nginx, Supervisor (worker management), and a Java 17 runtime for UBL XML validation and digital signature verification. --- ## Backend Structure The Symfony backend is organized into domain-oriented controllers, entities, and services: | Layer | Count | Examples | |-------|-------|---------| | Controllers | 68 | Invoices, Auth, E-Invoice, Webhooks, Admin | | Entities | 57 | Invoice, Client, Payment, Company, User | | Services | 95 | PDF generation, e-Factura sync, email, import | | Message handlers | 17 | Async PDF, ANAF submission, webhook dispatch | | Console commands | 21 | Cron jobs, maintenance, data migration | ### Key Service Domains - **Anaf/** (11 services) — UBL XML generation, ANAF SPV communication, XML parsing, validation - **Import/** (22 services) — Data migration from 12 invoicing systems (SmartBill, Ciel, eMag, etc.) - **Storage/** (5 services) — Multi-backend file storage with encryption (S3, local) - **Export/** (3 services) — CSV, Saga XML, ZIP archive exports - **Backup/** (3 services) — Company-level backup and restore --- ## Data Flow ### Invoice Lifecycle ``` Draft → Issued → Sent to ANAF → Validated/Rejected │ │ │ │ │ │ │ └─ Centrifugo → real-time UI update │ │ └─ Async: SubmitToAnafMessage (Redis queue) │ └─ Async: GeneratePdfMessage (Redis queue) └─ Sync: Validate + persist to MySQL ``` 1. **Create draft** — User creates invoice via API. Data validated and persisted to MySQL. 2. **Issue** — Generates UBL 2.1 XML, stores to S3, dispatches async PDF generation. 3. **Submit to provider** — Async worker uploads XML to e-invoice provider. Status set to `sent_to_provider`. 4. **Provider sync** — Cron job polls provider for status updates. Invoice marked `validated` or `rejected`. 5. **Real-time update** — Each status change publishes to Centrifugo. Connected clients update instantly. ### e-Factura Sync A cron job runs every 15 minutes to synchronize with ANAF: ``` Cron → EFacturaSyncService ├─ Check outgoing invoice statuses (validated / rejected) ├─ Download incoming invoices (auto-create Invoice + Client + Products) ├─ Create EFacturaMessage audit records └─ Publish real-time notifications via Centrifugo ``` Incoming invoices from suppliers are automatically parsed from UBL XML into full Invoice entities with line items. ### PDF Generation PDF generation uses a dual strategy for reliability: 1. **Primary**: HTTP request to Java service (~100ms) — UBL XML → PDF 2. **Fallback**: wkhtmltopdf shell command (~2–4s) — HTML template → PDF Both paths store the resulting PDF in S3 via Flysystem. --- ## Real-Time Updates Centrifugo provides WebSocket-based real-time updates to all connected clients. ### Channel Structure | Channel | Purpose | Features | |---------|---------|----------| | `invoices:{company}` | Invoice status changes | History (20 messages, 2 min TTL) | | `notifications:{user}` | User notifications | Presence, history (50 messages, 1 hour TTL), recovery | | `dashboard:{company}` | Dashboard stats | Presence tracking | | `user:{user}` | User-specific events | History (10 messages, 1 min TTL) | ### Publishing Flow The backend queues messages during request processing and flushes them at the end of the request lifecycle: ``` Controller / Message Handler ↓ CentrifugoService.queue(channel, data) ← buffer messages ↓ CentrifugoFlushSubscriber (kernel.terminate / console.terminate) ↓ CentrifugoService.flush() ← batch HTTP POST to Centrifugo ↓ Centrifugo broadcasts to subscribed clients ``` This batching approach minimizes HTTP calls to Centrifugo — a single request can carry updates for multiple channels. --- ## Async Job Processing Background jobs are processed via Symfony Messenger with Redis as the transport: | Message | Purpose | |---------|---------| | `SubmitToAnafMessage` | Upload invoice XML to ANAF | | `CheckAnafStatusMessage` | Poll ANAF for submission status | | `SyncCompanyMessage` | Full e-Factura sync for a company | | `GeneratePdfMessage` | Generate invoice PDF | | `GenerateZipExportMessage` | Create ZIP archive of invoices | | `SendExternalNotificationMessage` | Send push notifications | | `SendPushNotificationMessage` | Firebase push to mobile devices | | `ProcessImportMessage` | Import data from external systems | | `DispatchWebhookMessage` | Deliver webhook payloads | | `DeleteUserAccountMessage` | GDPR account deletion | | `DeleteCompanyDataMessage` | Company data removal | | `ResetCompanyDataMessage` | Company data reset | | `SendInvitationEmailMessage` | Organization invitations | | `SendEmailConfirmationMessage` | Email verification | Supervisor runs the Messenger worker process, which continuously polls the Redis queue and processes messages. --- ## Multi-Tenancy ``` Organization (root tenant) ├─ User[] (members with roles) └─ Company[] (business entities) ├─ Invoice[], ProformaInvoice[], DeliveryNote[] ├─ Client[], Supplier[], Product[] ├─ Payment[], BankAccount[] ├─ DocumentSeries[], VatRate[] ├─ WebhookEndpoint[] ├─ EmailTemplate[] └─ AnafToken (e-Factura credentials) ``` - **Organization** is the billing and membership boundary - **Company** is the data isolation boundary — all API requests require an `X-Company` header - Users can belong to one organization with access to multiple companies - RBAC with 5 roles (Owner, Admin, Accountant, Member, Viewer) and 40+ granular permissions See [Multi-Tenancy](/concepts/multi-tenancy) for details. --- ## Authentication Multiple authentication methods feed into a unified JWT token flow: ``` Email/Password ──┐ Google OAuth ────┤──→ [MFA Challenge?] ──→ JWT Token ──→ API Access Passkeys ────────┤ │ API Keys ────────┘ └─ TOTP or Backup Code ``` - Email/password and Google OAuth trigger MFA if enabled - Passkeys skip MFA (inherently multi-factor) - API keys bypass MFA (scoped programmatic access) - JWT tokens are RSA-signed (RS256) with 1-hour expiry and 30-day refresh tokens See [Authentication](/getting-started/authentication) for details. --- ## Deployment ### Docker (Recommended) Five containers managed via Docker Compose: ```bash docker compose up -d # backend, frontend, db, redis, centrifugo ``` ### Kubernetes Helm chart available with configurable resource limits, persistent volumes, and ingress rules. See [System Requirements](/getting-started/system-requirements) for resource allocation. ### CI/CD Each repository has its own GitHub Actions workflow that builds Docker images and deploys via SSH with a blue-green strategy: 1. Build and push images to GHCR 2. Start new containers on alternate ports 3. Run health checks 4. Swap traffic if healthy, rollback if not Backend and frontend are deployed from the monorepo (`stornoro/storno`). Docs and other services deploy independently from their own repositories. --- ## Document Lifecycle > Understanding the status transitions for invoices, proforma invoices, delivery notes, and credit notes. URL: https://docs.storno.ro/concepts/document-lifecycle # Document Lifecycle Each document type in Storno.ro has a defined set of statuses and allowed transitions. ## Invoice Lifecycle ``` ┌──────────┐ ┌────►│ cancelled │ │ └──────────┘ │ │ │ │ restore │ ▼ ┌───────┐ issue ┌────────┐ │ ┌───────┐ submit ┌─────────────┐ │ draft │────────►│ issued │──┘ │ draft │ │sent_to_provider│ │ │ │ │────────────────────────►│ │ └───────┘ └────────┘ └──────┬──────┘ │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ ┌───────────┐ ┌──────────┐ ┌──────────┐ │ validated │ │ rejected │ │ error │ └─────┬─────┘ └──────────┘ └──────────┘ │ ┌─────────┼──────────┐ ▼ ▼ ▼ ┌──────┐ ┌──────────────┐ ┌─────────┐ │ paid │ │partially_paid│ │ overdue │ └──────┘ └──────────────┘ └─────────┘ ``` ### Invoice Statuses | Status | Description | |--------|-------------| | `draft` | Initial state. Invoice can be edited and deleted. | | `issued` | Invoice has been validated and XML/PDF generated. Cannot be edited. | | `sent_to_provider` | XML uploaded to e-invoice provider. Awaiting processing. | | `validated` | E-invoice provider accepted the invoice. | | `rejected` | E-invoice provider rejected the invoice. Needs correction. | | `cancelled` | Invoice was cancelled. Can be restored to draft. | | `paid` | Full payment recorded (`amountPaid >= total`). | | `partially_paid` | Partial payment recorded (`0 < amountPaid < total`). | | `overdue` | Past due date with outstanding balance. | | `synced` | Incoming invoice synced from e-invoice provider (not created locally). | ### Key Rules - Only `draft` invoices can be edited or deleted - Issuing an invoice is irreversible (but it can be cancelled) - Cancelled invoices can be restored back to `draft` - Payment statuses (`paid`, `partially_paid`, `overdue`) are computed from payment records - Credit notes should be used to correct validated invoices ## Proforma Invoice Lifecycle ``` ┌───────┐ send ┌──────┐ accept ┌──────────┐ convert ┌───────────┐ │ draft │────────►│ sent │─────────►│ accepted │──────────►│ converted │ │ │ │ │ │ │ │(→ invoice)│ └───┬───┘ └──┬───┘ └──────────┘ └───────────┘ │ │ │ │ reject │ ▼ │ ┌──────────┐ │ │ rejected │ │ └──────────┘ │ │ cancel ▼ ┌───────────┐ │ cancelled │ └───────────┘ ``` ### Proforma Statuses | Status | Description | |--------|-------------| | `draft` | Initial state. Can be edited and deleted. | | `sent` | Proforma sent to client. Awaiting response. | | `accepted` | Client accepted the proforma. | | `rejected` | Client rejected the proforma. | | `converted` | Proforma converted to a real invoice. | | `cancelled` | Proforma cancelled. | ### Key Rules - Only `draft` proformas can be edited - Converting creates a new invoice with the same data - The `convertedInvoice` field links to the created invoice ## Delivery Note Lifecycle ``` ┌───────┐ issue ┌────────┐ convert ┌───────────┐ │ draft │────────►│ issued │──────────►│ converted │ │ │ │ │ │(→ invoice)│ └───┬───┘ └────┬───┘ └───────────┘ │ │ │ cancel │ cancel ▼ ▼ ┌───────────┐ ┌───────────┐ │ cancelled │ │ cancelled │ └───────────┘ └───────────┘ ``` ### Delivery Note Statuses | Status | Description | |--------|-------------| | `draft` | Initial state. Can be edited and deleted. | | `issued` | Delivery note issued. Cannot be edited. | | `converted` | Converted to an invoice. | | `cancelled` | Delivery note cancelled. | ## Credit Notes Credit notes use the Invoice entity with `isCreditNote: true`. They follow the same lifecycle as invoices but are specifically used to: - Correct errors in validated invoices - Issue partial or full refunds - Adjust quantities or prices Credit notes reference the original invoice via `parentDocumentId`. ## Payment Tracking Payments are tracked separately from invoice status: ```bash # Record a payment POST /api/v1/invoices/{uuid}/payments { "amount": 1000.00, "paymentDate": "2026-02-15", "paymentMethod": "bank_transfer" } ``` - When `amountPaid == total` → status becomes `paid` - When `0 < amountPaid < total` → status becomes `partially_paid` - When `dueDate < today && amountPaid < total` → status becomes `overdue` - Deleting a payment recalculates the status ## Status Transitions Summary | Action | From | To | |--------|------|----| | Issue invoice | `draft` | `issued` | | Submit to provider | `issued` | `sent_to_provider` | | Provider validates | `sent_to_provider` | `validated` | | Provider rejects | `sent_to_provider` | `rejected` | | Cancel | `draft`, `issued` | `cancelled` | | Restore | `cancelled` | `draft` | | Full payment | any active | `paid` | | Partial payment | any active | `partially_paid` | | Past due date | any unpaid | `overdue` | --- ## E-Invoicing (Multi-Country) > Multi-country e-invoicing support for Romania (ANAF), Germany (XRechnung), Italy (SDI), Poland (KSeF), and France (Factur-X). URL: https://docs.storno.ro/concepts/einvoice-integration # E-Invoicing (Multi-Country) Storno.ro supports e-invoicing across 5 EU countries through a unified API. Each country has its own XML format, validation rules, and government API, but the submission interface is the same. ## Supported Providers | Provider | Country | Format | Government System | |----------|---------|--------|-------------------| | `anaf` | Romania | UBL 2.1 (CIUS-RO) | ANAF SPV / e-Factura | | `xrechnung` | Germany | UBL 2.1 (XRechnung 3.0) | ZRE (Zentraler Rechnungseingang) | | `sdi` | Italy | FatturaPA XML v1.2 | SDI (Sistema di Interscambio) | | `ksef` | Poland | FA(2) XML | KSeF (Krajowy System e-Faktur) | | `facturx` | France | CII XML (Factur-X EN 16931) | Chorus Pro (B2G) | ## Architecture ### Unified Submission Flow All providers share the same submission endpoint: ```bash POST /api/v1/invoices/{uuid}/submit-einvoice Content-Type: application/json {"provider": "xrechnung"} ``` - **ANAF** (`provider: "anaf"`): Routes through the existing Romanian e-Factura flow (same behavior as `POST /invoices/{uuid}/submit`). Uses ANAF OAuth tokens, UBL XML generation, and SPV submission. - **Foreign providers**: Creates an `EInvoiceSubmission` record, generates country-specific XML, stores it via Flysystem, and optionally submits to the government API if credentials are configured. ### Two-Tier Processing Each foreign provider supports two modes: 1. **XML-only** (no API credentials): Generates compliant XML that can be downloaded and uploaded manually to the government portal. 2. **Full automation** (with API credentials): Generates XML, submits via API, and polls for status updates. ### Entity Model ``` Invoice ──→ EInvoiceSubmission (per provider) Company ──→ CompanyEInvoiceConfig (per provider) Client ──→ einvoiceIdentifiers (JSON field) ``` **EInvoiceSubmission** tracks each submission: - `provider`: Which system (xrechnung, sdi, ksef, facturx) - `status`: pending → submitted → accepted/rejected/error - `externalId`: Government system's tracking ID - `xmlPath`: Path to generated XML in storage - `metadata`: Provider-specific response data **CompanyEInvoiceConfig** stores per-company credentials: - `provider`: Which system - `enabled`: Active toggle - `config`: Provider-specific JSON (API keys, certificates, etc.) ## Provider Configuration ### Germany (XRechnung) For automated B2G submission via ZRE: ```bash # Save ZRE credentials POST /api/v1/companies/{uuid}/einvoice-config { "provider": "xrechnung", "enabled": true, "config": { "clientId": "your-zre-client-id", "clientSecret": "your-zre-client-secret" } } ``` Without credentials, XRechnung XML is generated and stored for manual upload. **Client requirements:** - Set `einvoiceIdentifiers.xrechnung.leitwegId` on the client for B2G routing (Leitweg-ID is mandatory per BR-DE-1). ### Italy (SDI) Supports direct submission (with digital certificate) or via intermediary: ```bash # Via intermediary POST /api/v1/companies/{uuid}/einvoice-config { "provider": "sdi", "enabled": true, "config": { "apiEndpoint": "https://intermediary.example.com/api", "apiKey": "your-api-key" } } ``` **Client requirements:** - Set `einvoiceIdentifiers.sdi.codiceDestinatario` (7-char routing code) for Italian B2B recipients. - Set `einvoiceIdentifiers.sdi.pecAddress` for individual recipients without a routing code. - Foreign recipients automatically get `XXXXXXX` as CodiceDestinatario. ### Poland (KSeF) ```bash POST /api/v1/companies/{uuid}/einvoice-config { "provider": "ksef", "enabled": true, "config": { "authToken": "your-ksef-auth-token", "nip": "1234567890" } } ``` ### France (Factur-X / Chorus Pro) Chorus Pro is for B2G (business-to-government) invoicing: ```bash POST /api/v1/companies/{uuid}/einvoice-config { "provider": "facturx", "enabled": true, "config": { "clientId": "your-piste-client-id", "clientSecret": "your-piste-client-secret", "siret": "12345678901234" } } ``` For B2B, Factur-X XML is generated and embedded in PDF/A-3 for direct exchange. ### Romania (ANAF) ANAF configuration is managed through the dedicated ANAF token system (not through `einvoice-config`). See [ANAF Integration](./anaf-integration). However, ANAF submissions can also be triggered through the unified endpoint: ```bash POST /api/v1/invoices/{uuid}/submit-einvoice {"provider": "anaf"} ``` This is equivalent to `POST /api/v1/invoices/{uuid}/submit`. ## Client E-Invoice Identifiers Clients can store per-provider routing identifiers in the `einvoiceIdentifiers` JSON field: ```json { "xrechnung": { "leitwegId": "04011000-1234512345-06" }, "sdi": { "codiceDestinatario": "ABCDEFG", "pecAddress": "client@pec.it" }, "facturx": { "serviceCode": "SERVICE-001" } } ``` Set via the client API: ```bash PATCH /api/v1/clients/{uuid} { "einvoiceIdentifiers": { "sdi": {"codiceDestinatario": "ABCDEFG"} } } ``` ## Submission Lifecycle ``` Submit → Validate → Generate XML → Store XML → (Optional) API Submit → Poll Status ``` 1. **Validate**: Country-specific validation rules are checked before XML generation. 2. **Generate XML**: Country-specific XML is generated (UBL, FatturaPA, FA(2), or CII). 3. **Store XML**: XML is stored in the company's Flysystem storage. 4. **API Submit** (optional): If credentials are configured, XML is submitted to the government API. 5. **Poll Status**: Async message handler polls for acceptance/rejection. ## Validation Rules Each provider has country-specific validation: - **XRechnung**: BR-DE rules (mandatory BuyerReference, seller Contact with Name/Phone/Email, SEPA payment codes) - **SDI**: FatturaPA rules (Partita IVA, Codice Fiscale, CodiceDestinatario routing, Natura codes) - **KSeF**: Polish rules (NIP format, mandatory Adnotacje section, VAT rate grouping) - **Factur-X**: French rules (SIREN/SIRET, CII element ordering, Chorus Pro SIRET) ## API Endpoints | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/api/v1/einvoice/providers` | List available providers | | `POST` | `/api/v1/invoices/{uuid}/submit-einvoice` | Submit to provider | | `GET` | `/api/v1/invoices/{uuid}/einvoice-submissions` | List submissions | | `GET` | `/api/v1/companies/{uuid}/einvoice-config` | List configs | | `POST` | `/api/v1/companies/{uuid}/einvoice-config` | Create/update config | | `DELETE` | `/api/v1/companies/{uuid}/einvoice-config/{provider}` | Delete config | ## Self-Service Provider Configuration Foreign e-invoice providers (XRechnung, SDI, KSeF, Factur-X) require API credentials that each business must obtain from their national authority or intermediary. Storno provides a self-service configuration flow: ### Credential Management 1. **Settings > E-Invoice Providers** — configure credentials for each provider 2. Credentials are encrypted at rest using `libsodium` symmetric encryption 3. API responses return masked values (e.g., `****ab12`) — raw credentials are never exposed 4. Password fields show "leave empty to keep current" when editing existing configs ### Connection Testing Before saving, users can test their credentials against the provider's API: - **XRechnung**: OAuth2 client credentials exchange with ZRE - **SDI**: Intermediary API status check, or certificate password validation - **KSeF**: Session initialization with auth token + NIP - **Factur-X**: OAuth2 client credentials exchange with Chorus Pro (PISTE) ### Provider Credential Requirements | Provider | Country | Credentials | |----------|---------|-------------| | ANAF | Romania | OAuth tokens (managed separately via e-Factura > ANAF Tokens) | | XRechnung | Germany | `clientId`, `clientSecret` (ZRE portal) | | SDI | Italy | Certificate password (direct) or `apiEndpoint` + `apiKey` (intermediary like Aruba/Namirial) | | KSeF | Poland | `authToken`, `nip` (from KSeF portal) | | Factur-X | France | `clientId`, `clientSecret`, `siret` (from PISTE/AIFE portal) | ### API Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/v1/companies/{uuid}/einvoice-config` | List configs (masked credentials) | | `POST` | `/api/v1/companies/{uuid}/einvoice-config` | Create/update config (encrypts on save) | | `POST` | `/api/v1/companies/{uuid}/einvoice-config/test` | Test connection | | `DELETE` | `/api/v1/companies/{uuid}/einvoice-config/{provider}` | Delete config | ### Migration from Plain-Text Existing plain-text configs are automatically used (legacy fallback). Run the one-time migration command to encrypt them: ```bash php bin/console app:einvoice:encrypt-configs ``` --- ## Licensing > How Storno.ro licensing works for SaaS and self-hosted deployments. URL: https://docs.storno.ro/concepts/licensing # Licensing Storno.ro supports two deployment models: **SaaS** (hosted at app.storno.ro) and **self-hosted** (Docker on your own infrastructure). The licensing system bridges both models, ensuring self-hosted instances receive the correct plan and features based on the owner's subscription. ## Deployment Modes ### SaaS Mode When you use Storno.ro at `app.storno.ro`, billing and plan management happen directly through Stripe. Your organization's subscription status is updated in real-time via webhooks. - Subscription managed via Stripe Checkout and Customer Portal - Plan changes take effect immediately - No license key needed ### Self-Hosted Mode When you deploy Storno.ro on your own infrastructure via Docker, a **license key** connects your instance to your SaaS subscription. The license key is a signed JWT that is validated entirely offline — no phone-home to the SaaS server is required. - Requires `LICENSE_KEY` environment variable - Validated offline via RSA signature verification (no network calls) - Plan, features, and expiration are embedded in the JWT - Billing (subscription changes, upgrades) redirects to the SaaS ## License Key Lifecycle ``` ┌─────────────────┐ Generate Key ┌──────────────────┐ │ SaaS Account │ ─────────────────────▶ │ License Key │ │ (Owner) │ │ (Signed JWT) │ └─────────────────┘ └──────────────────┘ │ Configure in Docker .env │ ▼ ┌──────────────────┐ │ Self-Hosted │ │ Instance │ │ (offline │ │ validation) │ └──────────────────┘ ``` ### 1. Generate The organization owner generates license keys from the SaaS dashboard (**Settings → Licensing**) or via the API (`POST /api/v1/licensing/keys`). Each key is a signed JWT containing the plan, features, and expiration. ### 2. Configure The key is placed in the self-hosted instance's `.env` file: ```bash LICENSE_KEY=eyJhbGciOiJSUzI1NiI...your-jwt-key ``` ### 3. Validate The instance validates the JWT signature locally using the embedded public key — no network request is made. Validation runs on startup and periodically via `app:license:sync`. ### 4. Sync On successful validation, the local organization's plan is updated to match the claims in the JWT. Features and limits are enforced locally. ### 5. Expire License keys have an expiration date embedded in the JWT. When a key expires, the instance falls back to the Community (free) plan. The owner can generate a new key from the SaaS dashboard. ## Plans & Features Storno.ro offers four plan tiers. Each tier includes everything from the tier below, plus additional features. ### Freemium (Free) Available to all users with no time limit. | Feature | Limit | |---------|-------| | e-Factura sync | Every 24 hours | | PDF generation | Yes | | Email sending | Yes | | Email templates | Yes | | Reports | Yes | | Exchange rates | Yes | | Companies | 1 | | Users per organization | 3 | | Invoices per month | 100 | ### Starter (19 RON/month) Everything in Freemium, plus: | Feature | Limit | |---------|-------| | e-Factura sync | Every 12 hours | | Payment links | Yes | | Mobile app | Yes | | Import / Export | Yes | | Companies | 3 | | Invoices per month | 500 | ### Professional (39 RON/month) Everything in Starter, plus: | Feature | Limit | |---------|-------| | e-Factura sync | Every 4 hours | | Invoices per month | Unlimited | | Recurring invoices | Yes | | Backup & restore | Yes | | Bank statements | Yes | | Webhooks | Yes | | Companies | 10 | | Users per organization | 10 | ### Business (69 RON/month) Everything in Professional, plus: | Feature | Limit | |---------|-------| | e-Factura sync | Every hour | | Companies | Unlimited | | Users per organization | Unlimited | | Realtime notifications | Yes | | White-label branding | Yes | | Self-hosting license | Yes | | Priority support | Yes | ### Feature Enforcement All plan limits are enforced at the API level. When a request requires a feature not available on the current plan, the API returns a `402 Payment Required` response with a `PLAN_LIMIT` error code: ```json { "error": "Recurring invoices are not available on your plan.", "code": "PLAN_LIMIT" } ``` Limits apply equally to web, mobile, and API key authenticated requests. ## Offline by Design License keys are signed JWTs validated entirely on your server. **No network calls are made to the SaaS server for license validation.** Your self-hosted instance works fully offline, air-gapped, or behind a firewall. | Scenario | Behavior | |----------|----------| | Valid JWT license | Plan and features applied from JWT claims | | Expired JWT license | Falls back to Community (free) plan | | Invalid or tampered JWT | Falls back to Community (free) plan | ## Data Privacy **No data is transmitted to the SaaS server for license validation.** The JWT is verified locally using cryptographic signature verification. Your data stays entirely on your self-hosted infrastructure. ## API Reference - [Validate License](/api-reference/licensing/validate) — Self-hosted validation endpoint (no auth) - [Create License Key](/api-reference/licensing/create-key) — Generate a new key (owner only) - [List License Keys](/api-reference/licensing/list-keys) — View all keys for your organization - [Revoke License Key](/api-reference/licensing/revoke-key) — Deactivate a key --- ## Multi-Tenancy > How organizations, companies, and resources are structured in the Storno.ro API. URL: https://docs.storno.ro/concepts/multi-tenancy # Multi-Tenancy Storno.ro uses a hierarchical multi-tenant architecture: **User → Organization → Company → Resources**. ## Hierarchy ``` User └── Organization (1 per user, created on registration) ├── Membership (role-based access) │ ├── Owner │ ├── Admin │ ├── Accountant │ └── Employee ├── Company A (CIF: RO12345678) │ ├── Invoices │ ├── Clients │ ├── Products │ ├── Suppliers │ ├── Bank Accounts │ ├── Document Series │ ├── VAT Rates │ └── Email Templates └── Company B (CIF: RO87654321) ├── Invoices └── ... ``` ## Organizations Every user belongs to exactly one organization. When a user registers, a default organization is created automatically. Organizations are the top-level tenant boundary. ## Companies Companies represent legal entities (identified by CIF/tax ID). Each organization can have multiple companies. ### Adding a Company Companies are added by CIF. Storno.ro validates the CIF against ANAF and auto-fills company details: ```bash curl -X POST https://api.storno.ro/api/v1/companies \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "cif": "RO12345678" }' ``` ## The X-Company Header Most API endpoints operate within a company context. You must include the `X-Company` header with a valid company UUID: ```bash curl https://api.storno.ro/api/v1/invoices \ -H "Authorization: Bearer {token}" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ### Endpoints That Don't Require X-Company These endpoints operate at the user or organization level: | Endpoint | Description | |----------|-------------| | `POST /api/auth` | Authentication | | `GET /api/v1/me` | Current user profile | | `PATCH /api/v1/me` | Update profile | | `GET /api/v1/companies` | List companies | | `POST /api/v1/companies` | Add company | | `GET /api/v1/anaf/tokens` | ANAF tokens | | `GET /api/v1/members` | Organization members | | `GET /api/v1/notifications` | Notifications | ## Membership Roles Users access an organization through memberships. Each membership has a role: | Role | Description | Permissions | |------|-------------|-------------| | **Owner** | Organization creator | Full access, cannot be deactivated | | **Admin** | Organization administrator | Full access, can manage members | | **Accountant** | Accounting staff | Access to assigned companies, can manage invoices | | **Employee** | Limited access | Read-only access to assigned companies | ### Company-Scoped Access Accountant and Employee roles can be restricted to specific companies within the organization: ```bash curl -X PATCH https://api.storno.ro/api/v1/members/{uuid} \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "role": "accountant", "allowedCompanies": ["{company_uuid_1}", "{company_uuid_2}"] }' ``` ## Invitations Organization owners and admins can invite users: ```bash curl -X POST https://api.storno.ro/api/v1/invitations \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "email": "accountant@example.com", "role": "accountant" }' ``` The invited user receives an email with a link to accept the invitation. If they don't have an account, they'll be prompted to register first. ## Data Isolation - Resources (invoices, clients, products, etc.) are scoped to a single company - Users can only access companies within their organization - API requests with an invalid or unauthorized `X-Company` header return `403 Forbidden` - Deleting a company cascades to all its resources (async operation) --- ## Point of Sale (POS) > How the Storno.ro POS module works — fiscal receipts, idempotency, refunds, SGR deposits, and offline sync. URL: https://docs.storno.ro/concepts/pos # Point of Sale (POS) The Storno.ro POS module turns the mobile app into a tablet- or phone-friendly cash register. It produces fiscal [Receipts](/objects/receipt), supports cash / card / meal-ticket / mixed payments, refunds, the Romanian SGR deposit scheme, and continues to operate while offline. The POS is built entirely on top of the public API — every action a cashier takes maps to one or more documented endpoints. This page summarises the moving parts and links to the reference docs for each. ## Components | Component | Purpose | |-----------|---------| | [Product](/objects/product) | Sellable item shown on the grid. `color`, `category`, and `sgrAmount` are the POS-specific fields. | | [ProductCategory](/objects/product-category) | Optional grouping shown as a chip strip above the grid. Tap a chip to filter the grid. | | [Receipt](/objects/receipt) | The fiscal document produced by every sale or refund. | | [Cash Register Movements](/api-reference/cash-register/movements-list) | Deposits, withdrawals, and adjustments to the till. Required for the daily Z-report. | ## Sale flow 1. Cashier loads the POS screen, which fetches active products + categories. 2. Items are tapped into a local cart. Per-line discounts, whole-cart discounts, and price overrides are applied client-side. 3. Cashier taps **Checkout** and selects payment method(s). Mixed payments split across `cashPayment`, `cardPayment`, and `otherPayment` (meal tickets count as `other`). 4. The app calls `POST /receipts` followed by `POST /receipts/{uuid}/issue` to issue and number the receipt. 5. PDF is rendered server-side. The mobile app keeps a local cache for offline preview. ## Idempotency Mobile networks drop packets. To make POS retries safe, every `POST /receipts` request from the mobile app includes an `Idempotency-Key` HTTP header — typically a UUID generated when the cashier taps **Checkout**. - The first request with a given key creates the receipt and stores the key on `Receipt.idempotencyKey`. - Subsequent requests with the same key return the **already-created** receipt instead of duplicating. - If both the `Idempotency-Key` header and an `idempotencyKey` body field are sent, the **header wins**. This makes it safe to retry on connection timeout, app restart, or even after the app comes back from being killed mid-checkout: the same key is replayed, and the server returns the original receipt. ## Refunds Refunds are first-class receipts. They mirror the parent's lines as **negative quantities** and invert the payment amounts so the daily Z-report nets out correctly. - `POST /receipts/{uuid}/refund` issues a refund. With no body, the entire receipt is refunded. - `lineSelections: [{ position, quantity }, ...]` performs a **partial refund**. Multiple partial refunds against the same parent are allowed until each line's quantity pool is exhausted. - Cancelling a refund (via `POST /receipts/{uuid}/cancel`) **releases its quantities back to the pool**, so the cashier can re-issue. - Refunds inherit `internalNote`, `cashRegisterName`, `fiscalNumber`, and customer fiscal data from the parent. - The PDF header reads `BON DE RAMBURSARE` (or the locale equivalent) instead of `BON FISCAL`. The parent receipt's status moves to `partially_refunded` while any quantity remains, and to `refunded` once everything has been refunded. See [Receipt → Refunds](/objects/receipt#refunds) for the data shape (`refundOf`, `refundedBy`). ## SGR — Sistem Garantie-Returnare Romania's SGR scheme charges a 0.50 RON refundable deposit on certain beverage containers. Storno's POS handles it transparently: - Set `Product.sgrAmount` (decimal string, e.g. `"0.50"`) on any product subject to the deposit. - When the product is sold, the POS auto-appends a **separate VAT-exempt line** for the deposit. The cashier doesn't add it manually. - Cancelling or refunding the parent line cancels/refunds the deposit line in lockstep. - The deposit line is flagged in the receipt PDF and renders below the parent line. ## Offline mode If the device loses connectivity mid-shift, the POS keeps working: 1. Sales are written to a local **outbox** keyed by their generated idempotency key. 2. The cashier sees the receipt rendered locally with a "pending sync" badge. 3. A background drainer flushes the outbox to the API as soon as the network is back; idempotency keys protect against duplicates if the server already received an earlier attempt. 4. ANAF e-Factura submission (where applicable) is queued separately and only fires once the receipt has a server-confirmed UUID. ## Cash register The till backing the POS is a [Bank Account](/objects/bank-account) with `type: "cash"`. Set `openingBalance` + `openingBalanceDate` for the day's opening float, and use `/cash-register/movements` for any cash in/out that isn't a sale (deposits, manager pickup, expense reimbursement). The daily Z-report reconciles opening balance + sales − refunds + deposits − withdrawals against the closing count. --- ## Recurring Invoices > How to set up automatic invoice generation on a schedule. URL: https://docs.storno.ro/concepts/recurring-invoices # Recurring Invoices Recurring invoices allow you to automatically generate invoices on a defined schedule. This is useful for subscription services, retainers, rent, and other periodic billing. ## How It Works 1. Create a recurring invoice template with client, lines, and schedule 2. Storno.ro generates invoices automatically on the scheduled dates 3. Optionally, invoices are emailed to the client automatically ## Frequencies | Frequency | Value | Description | |-----------|-------|-------------| | Weekly | `weekly` | Every week on a specific day | | Biweekly | `biweekly` | Every two weeks | | Monthly | `monthly` | Every month on a specific day | | Bimonthly | `bimonthly` | Every two months | | Quarterly | `quarterly` | Every three months | | Semiannual | `semiannual` | Every six months | | Annual | `annual` | Once per year | ### Frequency Day The `frequencyDay` parameter (1-31) specifies which day of the month (or week for weekly) the invoice should be generated. If the month has fewer days (e.g., February), the last day of the month is used. ### Frequency Month For `annual` frequency, `frequencyMonth` (1-12) specifies the month. ## Creating a Recurring Invoice ```bash curl -X POST https://api.storno.ro/api/v1/recurring-invoices \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" \ -H "Content-Type: application/json" \ -d '{ "clientId": "{client_uuid}", "seriesId": "{series_uuid}", "frequency": "monthly", "frequencyDay": 1, "nextIssuanceDate": "2026-03-01", "currency": "RON", "dueDateType": "relative", "dueDateDays": 30, "notes": "Servicii lunar de mentenanță IT", "lines": [ { "description": "Mentenanță IT - lunar", "quantity": 1, "unitPrice": 2000, "vatRateId": "{vat_rate_uuid}", "unitOfMeasure": "buc" } ] }' ``` ## Due Date Configuration | Type | Parameter | Description | |------|-----------|-------------| | `relative` | `dueDateDays` | Due date is N days after issue date | | `fixed` | `dueDateFixedDay` | Due date is always on a specific day of the month | ## Stop Date Set a `stopDate` to automatically stop generating invoices after a certain date: ```json { "stopDate": "2026-12-31" } ``` After the stop date, no more invoices will be generated, but the recurring invoice remains in the system. ## Auto-Email When enabled, generated invoices can be automatically emailed to the client: ```json { "autoEmailEnabled": true, "autoEmailTime": "09:00", "autoEmailDayOffset": 0 } ``` | Parameter | Description | |-----------|-------------| | `autoEmailEnabled` | Enable automatic emailing | | `autoEmailTime` | Time of day to send (HH:mm format) | | `autoEmailDayOffset` | Days after invoice generation to send (0 = same day) | ## Price Rules Recurring invoice lines support dynamic pricing: | Rule | Description | |------|-------------| | `fixed` | Use the specified `unitPrice` as-is | | `exchange_rate` | Convert from `referenceCurrency` to invoice currency using BNR rate | | `markup` | Apply `markupPercent` on top of the exchange rate price | ### Exchange Rate Example Bill in RON but base price on EUR rate: ```json { "lines": [ { "description": "Hosting - monthly", "quantity": 1, "unitPrice": 100, "priceRule": "exchange_rate", "referenceCurrency": "EUR" } ] } ``` The actual price will be calculated using the BNR exchange rate on the day of generation. ## Managing Recurring Invoices ### Toggle Active/Inactive ```bash curl -X POST https://api.storno.ro/api/v1/recurring-invoices/{uuid}/toggle \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" ``` ### Manual Trigger Generate an invoice immediately without waiting for the schedule: ```bash curl -X POST https://api.storno.ro/api/v1/recurring-invoices/{uuid}/issue-now \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" ``` ## Tracking Each recurring invoice tracks: | Field | Description | |-------|-------------| | `nextIssuanceDate` | When the next invoice will be generated | | `lastIssuedAt` | When the last invoice was generated | | `lastInvoiceNumber` | Number of the most recently generated invoice | | `isActive` | Whether generation is enabled | ## Document Type Recurring invoices can generate either regular invoices or credit notes: ```json { "documentType": "invoice" } ``` | Value | Description | |-------|-------------| | `invoice` | Generate regular invoices (default) | | `credit_note` | Generate credit notes | --- ## Series & Numbering > How document series and automatic numbering work in Storno.ro. URL: https://docs.storno.ro/concepts/series-numbering # Series & Numbering Storno.ro uses document series to generate sequential, formatted document numbers for invoices, proforma invoices, credit notes, and delivery notes. ## Document Series A document series defines a numbering pattern for a specific document type within a company. ### Series Properties | Property | Description | |----------|-------------| | `prefix` | The series prefix (e.g., `FACT`, `PRO`, `CN`, `AVZ`) | | `currentNumber` | The last used number in this series | | `type` | Document type: `invoice`, `proforma`, `credit_note`, `delivery_note` | | `active` | Whether this series is available for new documents | | `nextNumber` | Computed: the next number to be assigned (e.g., `FACT-001`) | ### How Numbering Works When a document is created with a series, the system: 1. Increments `currentNumber` by 1 2. Formats the number with zero-padding (e.g., `001`, `002`) 3. Combines prefix + separator + padded number 4. Assigns the result as the document `number` **Example:** Series with prefix `FACT` and currentNumber `42`: - Next document number: `FACT-043` ### Creating a Series ```bash curl -X POST https://api.storno.ro/api/v1/document-series \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" \ -H "Content-Type: application/json" \ -d '{ "prefix": "FACT", "type": "invoice", "currentNumber": 0 }' ``` ### Multiple Series A company can have multiple active series per document type. This is useful for: - **Different business lines** — `FACT-A` for consulting, `FACT-B` for products - **Yearly series** — `FACT2026`, `FACT2025` - **Branch offices** — `BUC-`, `CLJ-` ### Series Uniqueness The prefix must be unique within a company for each document type. You cannot have two invoice series with the same prefix. ## Series Types | Type | Used For | Example Prefix | |------|----------|----------------| | `invoice` | Invoices and credit notes | `FACT`, `FCT`, `INV` | | `proforma` | Proforma invoices | `PRO`, `PF` | | `credit_note` | Credit notes | `CN`, `NC` | | `delivery_note` | Delivery notes | `AVZ`, `DN` | ## Managing Series ### Adjusting the Counter If you need to skip numbers or align with an existing series: ```bash curl -X PATCH https://api.storno.ro/api/v1/document-series/{uuid} \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" \ -H "Content-Type: application/json" \ -d '{ "currentNumber": 100 }' ``` The next document will be numbered 101. ### Deactivating a Series Deactivated series cannot be used for new documents but existing documents retain their numbers: ```bash curl -X PATCH https://api.storno.ro/api/v1/document-series/{uuid} \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" \ -H "Content-Type: application/json" \ -d '{ "active": false }' ``` ## Auto-Created Series When a company is added, Storno.ro may auto-create default series based on common Romanian conventions. These have `source: "auto"` and can be modified or deleted. ## Best Practices - Use short, meaningful prefixes (3-5 characters) - Create separate series per document type - Don't change `currentNumber` downward — this can cause duplicate numbers - Deactivate old series instead of deleting them (preserves document references) --- ## Webhooks & Events > Outbound HTTP webhooks, real-time WebSocket events, and in-app notification system. URL: https://docs.storno.ro/concepts/webhooks-events # Webhooks & Events Storno.ro delivers real-time updates through three complementary channels: **outbound HTTP webhooks** for server-to-server integrations, **WebSocket connections** (Centrifugo) for live UI updates, and **in-app notifications** for user-facing alerts. --- ## Outbound Webhooks Outbound webhooks send HTTP POST requests to your server whenever business events occur. Use them to integrate Storno.ro with ERPs, accounting tools, Slack, or any system that accepts HTTP callbacks. ### How it works 1. You [create a webhook endpoint](/api-reference/webhooks/create-webhook) with a destination URL and the events you want to receive 2. Storno.ro generates an HMAC-SHA256 signing secret (shown only once — store it securely) 3. When a subscribed event occurs, Storno.ro sends a signed POST request to your URL 4. Your server verifies the signature and processes the payload 5. If delivery fails, Storno.ro retries up to 3 times with exponential backoff ### Webhook payload format Every webhook delivery sends a JSON payload with this structure: ```json { "id": "0192b3a4-5c6d-7e8f-9a0b-1c2d3e4f5a6b", "event": "invoice.validated", "created_at": "2026-02-19T10:30:00+00:00", "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "number": "UEP2026000002", "status": "validated", "direction": "outgoing", "total": "30940.00", "currency": "RON" } } ``` | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique delivery ID (UUID v7) | | `event` | string | Event type that triggered the webhook | | `created_at` | string | ISO 8601 timestamp of when the event occurred | | `data` | object | Event-specific payload (varies by event type) | ### HTTP headers Each delivery includes these headers: | Header | Description | |--------|-------------| | `Content-Type` | `application/json` | | `X-Webhook-Signature` | HMAC-SHA256 hex digest of the raw body, signed with your secret | | `X-Webhook-Event` | Event type name (e.g., `invoice.validated`) | | `X-Webhook-Id` | Unique delivery ID (same as payload `id`) | | `User-Agent` | `Storno-Webhook/1.0` | ### Verifying signatures Always verify the `X-Webhook-Signature` header to ensure the request came from Storno.ro. Compute the HMAC-SHA256 of the raw request body using your signing secret and compare: ```javascript const crypto = require('crypto'); function verifyWebhook(rawBody, signatureHeader, secret) { const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signatureHeader) ); } // Express middleware example app.post('/webhooks/storno', (req, res) => { const signature = req.headers['x-webhook-signature']; const isValid = verifyWebhook(req.rawBody, signature, process.env.WEBHOOK_SECRET); if (!isValid) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.rawBody); console.log(`Received ${event.event}:`, event.data); res.status(200).send('OK'); }); ``` ```php $payload = file_get_contents('php://input'); $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? ''; $secret = getenv('WEBHOOK_SECRET'); $expected = hash_hmac('sha256', $payload, $secret); if (!hash_equals($expected, $signature)) { http_response_code(401); exit('Invalid signature'); } $event = json_decode($payload, true); // Process $event['event'] and $event['data'] http_response_code(200); ``` ```python import hmac import hashlib def verify_webhook(payload: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature) ``` Always use constant-time comparison (`timingSafeEqual`, `hash_equals`, `hmac.compare_digest`) to prevent timing attacks. ### Retry policy If your endpoint returns a non-2xx status code or the connection fails, Storno.ro retries with exponential backoff: | Attempt | Delay | Total elapsed | |---------|-------|---------------| | 1 | Immediate | 0 | | 2 | 1 minute | 1 min | | 3 | 5 minutes | 6 min | After 3 failed attempts the delivery is marked as `failed` and no further retries are attempted. You can monitor delivery status and errors through the [delivery log endpoints](/api-reference/webhooks/list-deliveries). **Best practices for your endpoint:** - Return `200 OK` as quickly as possible — process the payload asynchronously - Respond within 10 seconds (the request times out after that) - Handle duplicate deliveries idempotently using the `id` field - If you return a 2xx status, the delivery is considered successful ### Delivery statuses | Status | Description | |--------|-------------| | `pending` | Delivery queued, not yet attempted | | `success` | Your endpoint returned a 2xx response | | `retrying` | Delivery failed, retry scheduled | | `failed` | All retry attempts exhausted | --- ## Event Types Storno.ro supports 15 event types organized into 5 categories. Retrieve the full list from the API via [`GET /api/v1/webhooks/events`](/api-reference/webhooks/list-events). ### Invoice events | Event | Description | Payload fields | |-------|-------------|----------------| | `invoice.created` | New invoice created or synced from e-invoice provider | `id`, `number`, `status`, `direction`, `total`, `currency` | | `invoice.issued` | Invoice issued (draft finalized) | `id`, `number`, `status`, `direction`, `total`, `currency` | | `invoice.validated` | E-invoice provider validated an outgoing invoice | `id`, `number`, `status`, `direction`, `total`, `currency` | | `invoice.rejected` | E-invoice provider rejected an outgoing invoice | `id`, `number`, `status`, `direction`, `total`, `currency` | | `invoice.sent_to_provider` | Invoice submitted to e-invoice provider | `id`, `number`, `status`, `direction`, `total`, `currency` | ### Company events | Event | Description | Payload fields | |-------|-------------|----------------| | `company.created` | New company added to the organization | `id`, `name`, `cif` | | `company.updated` | Company data modified | `id`, `name`, `cif` | | `company.removed` | Company soft-deleted | `id`, `name`, `cif` | | `company.restored` | Company restored from soft-delete | `id`, `name`, `cif` | | `company.reset` | Company data reset (invoices, clients cleared) | `id`, `name`, `cif` | ### Sync events | Event | Description | Payload fields | |-------|-------------|----------------| | `sync.started` | E-invoice sync process started | `company_id`, `cif` | | `sync.completed` | E-invoice sync finished successfully | `company_id`, `cif`, `invoices_synced` | | `sync.error` | E-invoice sync encountered an error | `company_id`, `cif`, `error` | ### Payment events | Event | Description | Payload fields | |-------|-------------|----------------| | `payment.received` | Payment recorded on an invoice | `id`, `invoice_id`, `amount`, `currency`, `payment_method` | ### Provider Authentication Events | Event | Description | Payload fields | |-------|-------------|----------------| | `anaf.token_created` | New ANAF OAuth token obtained | `company_id`, `cif`, `expires_at` | --- ## Webhook Management ### Creating a webhook ```bash curl -X POST https://api.storno.ro/api/v1/webhooks \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.example.com/webhooks", "events": ["invoice.created", "invoice.validated", "payment.received"], "description": "ERP integration" }' ``` The signing `secret` is returned in full only on creation and on [regenerate-secret](/api-reference/webhooks/regenerate-secret). Store it securely — all subsequent reads return a masked value. ### Testing a webhook Send a test delivery to verify your endpoint is reachable: ```bash curl -X POST https://api.storno.ro/api/v1/webhooks/{uuid}/test \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" ``` The test sends a `webhook.test` event synchronously and returns the HTTP result immediately: ```json { "success": true, "statusCode": 200, "durationMs": 145, "error": null } ``` ### Viewing delivery history ```bash curl https://api.storno.ro/api/v1/webhooks/{uuid}/deliveries?page=1&limit=20 \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" ``` Each delivery record includes the event type, HTTP status code, response time, attempt number, and any error message. Use the [delivery detail endpoint](/api-reference/webhooks/get-delivery) to inspect the full request payload and response body. ### Permissions | Permission | Roles | Actions | |------------|-------|---------| | `webhook.view` | Admin, Accountant | List endpoints, view details, view delivery log | | `webhook.manage` | Admin | Create, update, delete, test, regenerate secret | ### API endpoints | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | [`/api/v1/webhooks/events`](/api-reference/webhooks/list-events) | List available event types | | `GET` | [`/api/v1/webhooks`](/api-reference/webhooks/list-webhooks) | List webhook endpoints | | `POST` | [`/api/v1/webhooks`](/api-reference/webhooks/create-webhook) | Create a webhook endpoint | | `GET` | [`/api/v1/webhooks/{uuid}`](/api-reference/webhooks/get-webhook) | Get endpoint details | | `PATCH` | [`/api/v1/webhooks/{uuid}`](/api-reference/webhooks/update-webhook) | Update endpoint | | `DELETE` | [`/api/v1/webhooks/{uuid}`](/api-reference/webhooks/delete-webhook) | Delete endpoint | | `POST` | [`/api/v1/webhooks/{uuid}/test`](/api-reference/webhooks/test-webhook) | Send test delivery | | `POST` | [`/api/v1/webhooks/{uuid}/regenerate-secret`](/api-reference/webhooks/regenerate-secret) | Regenerate signing secret | | `GET` | [`/api/v1/webhooks/{uuid}/deliveries`](/api-reference/webhooks/list-deliveries) | List delivery history | | `GET` | [`/api/v1/webhooks/{uuid}/deliveries/{id}`](/api-reference/webhooks/get-delivery) | Get delivery detail | --- ## Real-Time Updates (Centrifugo) For live UI updates (browser and mobile), Storno.ro uses [Centrifugo](https://centrifugal.dev/) WebSocket connections. This is separate from outbound webhooks and intended for front-end applications. ### Connecting 1. Obtain a connection token: ```bash curl -X POST https://api.storno.ro/api/v1/centrifugo/connection-token \ -H "Authorization: Bearer {token}" ``` 2. Connect to the Centrifugo WebSocket server with the returned token 3. Subscribe to channels: ```bash curl -X POST https://api.storno.ro/api/v1/centrifugo/subscription-token \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "channel": "user:{user_id}" }' ``` ### WebSocket event types Events are published to user-specific and company-specific channels: | Event | Description | |-------|-------------| | `invoice.created` | New invoice created (including synced from e-invoice provider) | | `invoice.updated` | Invoice status or data changed | | `invoice.paid` | Payment recorded on invoice | | `sync.completed` | E-invoice sync finished | | `sync.error` | E-invoice sync encountered an error | | `notification.new` | New notification for the user | --- ## Notifications Storno.ro sends user-facing notifications through multiple channels. ### Channels | Channel | Description | |---------|-------------| | `in_app` | In-app notifications (visible in notification panel) | | `email` | Email notifications | | `push` | Push notifications (iOS, Android, Web) | ### Notification types | Type | Description | |------|-------------| | `invoice.validated` | E-invoice provider validated an outgoing invoice | | `invoice.rejected` | E-invoice provider rejected an outgoing invoice | | `invoice.overdue` | Invoice past its due date | | `payment.received` | Payment recorded on an invoice | | `invoice.issued` | Invoice issued | | `invoice.paid` | Invoice fully paid | ### Managing preferences Users can control which notifications they receive on which channels: ```bash # Get current preferences curl https://api.storno.ro/api/v1/notification-preferences \ -H "Authorization: Bearer {token}" # Update preferences curl -X PUT https://api.storno.ro/api/v1/notification-preferences \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "preferences": { "invoiceValidated": { "email": true, "inApp": true, "push": false }, "invoiceRejected": { "email": true, "inApp": true, "push": true }, "paymentReceived": { "email": true, "inApp": true, "push": false } } }' ``` ### Push notifications To receive push notifications, register a device token: ```bash curl -X POST https://api.storno.ro/api/v1/devices \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "token": "firebase_device_token_here", "platform": "android" }' ``` Supported platforms: `ios`, `android`, `web`. --- ## Invoice Events (Audit Log) Each invoice maintains a history of status changes and significant events: ```bash curl https://api.storno.ro/api/v1/invoices/{uuid}/events \ -H "Authorization: Bearer {token}" \ -H "X-Company: {company_uuid}" ``` ```json [ { "type": "status_change", "status": "issued", "timestamp": "2026-02-15T10:30:00Z", "details": "Invoice issued by user@example.com" }, { "type": "status_change", "status": "sent_to_provider", "timestamp": "2026-02-15T10:31:00Z", "details": "Submitted to ANAF (upload ID: 12345)" }, { "type": "status_change", "status": "validated", "timestamp": "2026-02-15T11:00:00Z", "details": "Validated by ANAF" } ] ``` --- ## CLI / MCP Server > Use the Storno.ro MCP server to manage invoices through AI assistants like Claude Code, Cursor, and Windsurf. URL: https://docs.storno.ro/integrations/cli # CLI / MCP Server The Storno CLI is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that exposes the full Storno.ro API as AI-callable tools. It lets you manage invoices, clients, payments, and e-invoice submissions through natural language in any MCP-compatible AI assistant. ## Features - **228 tools** covering the complete e-invoicing workflow - Works with **Claude Code**, **Claude Desktop**, **Cursor**, **Windsurf**, and any MCP client - Automatic JWT token refresh - Multi-company support with context switching - File uploads and binary downloads (PDF, XML, exports) ## Hosted Server (recommended) The fastest way to get started — no installation, no API keys. Storno hosts a public MCP server at **https://mcp.storno.ro/mcp** with OAuth authentication. Visit **[mcp.storno.ro](https://mcp.storno.ro)** for setup instructions and a one-click "Add to Claude" button. ### Claude (claude.ai) 1. Copy the URL: `https://mcp.storno.ro/mcp` 2. Go to [claude.ai/settings/connectors](https://claude.ai/settings/connectors) 3. Click "Add" and paste the URL 4. Authorize via OAuth ### Claude Code ```bash claude mcp add storno --transport http https://mcp.storno.ro/mcp ``` ### Cursor / Windsurf ```json { "mcpServers": { "storno": { "url": "https://mcp.storno.ro/mcp" } } } ``` ### Claude Desktop ```json { "mcpServers": { "storno": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.storno.ro/mcp"] } } } ``` ## Self-hosted / Token-based Setup For self-hosted Storno instances or if you prefer using JWT tokens directly. ### Installation ```bash npm install -g storno-cli ``` Requires Node.js 20 or later. ### Configuration The CLI uses environment variables for authentication and configuration: | Variable | Default | Description | |----------|---------|-------------| | `STORNO_BASE_URL` | `https://api.storno.ro` | API endpoint | | `STORNO_TOKEN` | — | JWT access token | | `STORNO_REFRESH_TOKEN` | — | Refresh token for automatic rotation | | `STORNO_COMPANY_ID` | — | Default company UUID | | `STORNO_EMAIL` | — | Email for auto-login on startup | | `STORNO_PASSWORD` | — | Password for auto-login on startup | ### Authentication methods **1. Pre-configured token** (recommended for production): Set `STORNO_TOKEN` and optionally `STORNO_REFRESH_TOKEN`. The server auto-refreshes expired tokens. **2. Auto-login on startup:** Set `STORNO_EMAIL` and `STORNO_PASSWORD`. The server authenticates automatically when it starts. **3. Interactive login:** Call the `auth_login` tool from your AI assistant with your credentials. ### IDE Setup #### Claude Code / Claude Desktop Add to your MCP configuration (`~/.claude/claude_desktop_config.json` or project `.mcp.json`): ```json { "mcpServers": { "storno": { "command": "storno-cli", "env": { "STORNO_TOKEN": "your-jwt-token", "STORNO_COMPANY_ID": "company-uuid" } } } } ``` #### Cursor Add to `.cursor/mcp.json` in your project: ```json { "mcpServers": { "storno": { "command": "storno-cli", "env": { "STORNO_TOKEN": "your-jwt-token", "STORNO_COMPANY_ID": "company-uuid" } } } } ``` #### Windsurf Add to your Windsurf MCP settings: ```json { "mcpServers": { "storno": { "command": "storno-cli", "env": { "STORNO_TOKEN": "your-jwt-token", "STORNO_COMPANY_ID": "company-uuid" } } } } ``` ## Tool Categories The 228 tools are organized into these categories: ### Authentication (7 tools) | Tool | Description | |------|-------------| | `auth_login` | Authenticate with email and password | | `auth_register` | Create a new user account | | `auth_refresh` | Rotate expired JWT tokens | | `auth_me` | Get current user profile | | `auth_update_profile` | Update name, phone, timezone, password | | `auth_forgot_password` | Request password reset email | | `auth_reset_password` | Complete password reset with token | ### Companies (8 tools) | Tool | Description | |------|-------------| | `companies_list` | List all companies with e-invoice sync status | | `companies_get` | Get company details | | `companies_create` | Create company via CIF lookup | | `companies_update` | Modify company settings | | `companies_delete` | Delete company and all data | | `companies_upload_logo` | Upload company logo (PNG/JPG/SVG, max 2MB) | | `companies_delete_logo` | Remove company logo | | `companies_select` | Set active company context for the session | ### Invoices (21 tools) | Tool | Description | |------|-------------| | `invoices_list` | Filter by status, date, client with pagination | | `invoices_get` | Full invoice details with line items and payments | | `invoices_create` | Create a draft invoice | | `invoices_update` | Edit a draft invoice | | `invoices_delete` | Delete a draft invoice | | `invoices_issue` | Finalize and generate UBL XML + PDF | | `invoices_submit` | Submit to e-invoice provider | | `invoices_validate` | Pre-flight UBL validation (quick or full) | | `invoices_cancel` | Cancel with reason | | `invoices_restore` | Undo cancellation | | `invoices_pdf` | Download PDF (base64) | | `invoices_xml` | Download UBL 2.1 XML | | `invoices_email` | Send invoice with attachments | | `invoices_email_defaults` | Get pre-filled email subject/body | | `invoices_email_history` | Track delivery status | | `invoices_events` | Timeline of status changes | | `invoices_attachments` | Download file attachments | | `invoices_payment` | Toggle paid/unpaid status | | `invoices_verify_signature` | Validate ANAF digital signature | | `invoices_advance_payment` | Create advance/prepayment invoice | | `invoices_correct_invoice` | Issue correction/debit note | ### Proforma Invoices (10 tools) | Tool | Description | |------|-------------| | `proforma_invoices_list` | List draft quotes | | `proforma_invoices_get` | Get proforma details | | `proforma_invoices_create` | Create a draft quote | | `proforma_invoices_update` | Modify a draft | | `proforma_invoices_send` | Email to client | | `proforma_invoices_accept` | Mark as accepted | | `proforma_invoices_reject` | Mark as rejected | | `proforma_invoices_convert` | Convert to regular invoice | | `proforma_invoices_delete` | Remove draft | | `proforma_invoices_pdf` | Download PDF | ### Recurring Invoices (7 tools) | Tool | Description | |------|-------------| | `recurring_invoices_list` | List schedules | | `recurring_invoices_get` | Schedule details | | `recurring_invoices_create` | Define recurrence (daily/weekly/monthly/yearly) | | `recurring_invoices_update` | Modify schedule | | `recurring_invoices_delete` | Disable auto-generation | | `recurring_invoices_pause` | Temporarily pause | | `recurring_invoices_resume` | Resume after pause | ### Delivery Notes (8 tools) | Tool | Description | |------|-------------| | `delivery_notes_list` | Track shipments | | `delivery_notes_get` | Delivery details | | `delivery_notes_create` | Create shipping document | | `delivery_notes_update` | Modify draft | | `delivery_notes_issue` | Finalize delivery note | | `delivery_notes_delete` | Remove draft | | `delivery_notes_pdf` | Download PDF | | `delivery_notes_xml` | Download UBL XML | ### ANAF Integration (7 tools) | Tool | Description | |------|-------------| | `anaf_status` | Check e-Factura token validity | | `anaf_tokens` | List ANAF OAuth tokens by CIF | | `anaf_create_token_link` | Generate device auth flow URL | | `anaf_delete_token` | Revoke token | | `anaf_validate_cif` | Verify token access for a CIF | | `anaf_sync_trigger` | Manually trigger sync from ANAF SPV | | `anaf_sync_status` | Check last sync timestamp and counts | ### Webhooks (10 tools) | Tool | Description | |------|-------------| | `webhooks_list` | List event subscribers | | `webhooks_get` | Webhook details | | `webhooks_create` | Register endpoint with event types | | `webhooks_update` | Modify URL, events, active status | | `webhooks_delete` | Remove webhook | | `webhooks_test` | Send test payload | | `webhooks_logs` | Recent delivery history | | `webhooks_regenerate_secret` | Rotate signing secret | | `webhooks_retry` | Retry failed deliveries | | `webhooks_clear_logs` | Clear old delivery logs | ### Other Tools | Category | Tools | Description | |----------|-------|-------------| | Clients | 2 | List and get client details | | Products | 2 | Product catalog management | | Payments | 3 | Record and manage payments | | Receipts | — | Receipt (bon fiscal) management | | Credit Notes | — | Credit note operations | | Suppliers | 4 | Supplier catalog | | Bank Accounts | 4 | Bank account configuration | | Document Series | 4 | Invoice number series | | VAT Rates | 4 | VAT rate management | | Email Templates | 4 | Email template customization | | Exchange Rates | 2 | BNR currency conversion | | API Keys | 5 | API key management with scopes | | Members | 3 | Team management | | Invitations | 4 | User invitations | | Notifications | 4 | Notification preferences | | Reports | 1 | VAT summary reports | | Exports | 1 | Data export (CSV/Excel/JSON) | | Dashboard | 1 | Revenue and invoice statistics | | Admin | 3 | System administration | | Licensing | 4 | License key management | ## Multi-Company Support All company-scoped tools support three ways to set the company context: 1. **Environment variable** — Set `STORNO_COMPANY_ID` at startup 2. **Runtime selection** — Call `companies_select` with a company UUID 3. **Per-request override** — Pass `companyId` as a parameter to any tool ## Example Workflows ### Create and email an invoice ``` You: "Create an invoice for Acme SRL, 10 hours of web development at 100 RON/hour, due in 30 days" The AI will: 1. Call clients_list to find Acme SRL 2. Call invoices_create with the line items and due date 3. Call invoices_issue to finalize 4. Call invoices_email to send it ``` ### Check ANAF sync status ``` You: "What's our ANAF sync status?" The AI will: 1. Call anaf_status to check token validity 2. Call anaf_sync_status to see last sync time and counts ``` ### Switch company context ``` You: "Switch to ABC Company" The AI will: 1. Call companies_list to find the company 2. Call companies_select to set it as active ``` ### Generate a monthly report ``` You: "Show me a VAT summary for January 2026" The AI will: 1. Call reports_vat with the date range 2. Present the summary with totals by VAT rate ``` --- ## Mobile App > Manage invoices, clients, and e-invoice submissions on the go with the Storno.ro mobile app for iOS and Android. URL: https://docs.storno.ro/integrations/mobile-app # Mobile App The Storno.ro mobile app gives you full access to your invoicing workflow from your phone. Available for iOS and Android. ## Features - **Invoice management** — create, view, edit, issue, and email invoices - **Proforma invoices** — create quotes and convert to invoices - **Delivery notes** — manage shipping documents - **Receipts** — track bonuri fiscale (Romanian fiscal receipts) - **Recurring invoices** — set up automated invoice schedules - **Client and supplier management** — full CRUD with search - **Product catalog** — manage your products and services - **Real-time sync** — live updates via WebSocket (Centrifugo) - **Push notifications** — get notified about e-invoice validation, payments, and more - **Multi-company support** — switch between companies instantly - **Biometric authentication** — Face ID on iOS, fingerprint on Android - **Offline support** — cached data available without network ## Authentication The app supports multiple login methods: | Method | Description | |--------|-------------| | Email + password | Standard authentication | | Google Sign-In | OAuth via Google account | | Passkeys | WebAuthn-based passwordless login | | Biometric | Face ID / fingerprint unlock after initial login | | MFA | Optional TOTP-based two-factor authentication with backup codes | After initial login, you can enable biometric unlock so subsequent app opens only require a fingerprint or face scan. ## Dashboard The home screen shows: - **Revenue statistics** — totals and trends - **Recent invoices** — quick access to latest activity - **E-invoice sync status** — current sync state and last sync time - **Company selector** — switch between companies from the header ## Invoice Workflow ### Creating an invoice 1. Tap **+** from the invoices tab 2. Select a client (search by name or CIF) 3. Add line items from your product catalog or create custom ones 4. Set dates, currency, payment terms, and series 5. Save as draft or issue immediately ### Issuing and submitting - **Issue** — finalizes the invoice, generates UBL XML and PDF - **Submit to provider** — sends the XML to e-invoice provider (requires valid provider token) - **Email** — send the invoice to your client with PDF/XML attachments ### Status tracking The app displays the full invoice lifecycle with status badges: | Status | Description | |--------|-------------| | `draft` | Editable, not yet finalized | | `issued` | Finalized with XML and PDF generated | | `sent_to_provider` | Submitted to e-invoice provider | | `validated` | Provider accepted the invoice | | `rejected` | Provider rejected -- tap to see errors | | `cancelled` | Cancelled with reason | ## Real-Time Updates The app connects to Centrifugo WebSocket for live updates: - New invoices appear instantly when synced from e-invoice provider - Status changes (validated, rejected) update in real-time - Payment recordings reflect immediately - Company data changes sync across devices ### Self-hosted instances The mobile app derives the WebSocket URL automatically from the server host (e.g. `https://factura.yourdomain.com` → `wss://factura.yourdomain.com/connection/websocket`). No additional configuration is needed — just ensure your reverse proxy forwards WebSocket upgrades on `/connection/websocket`. See [Self-Hosting](/getting-started/self-hosting#websocket-not-connecting) for details. ## Push Notifications Register your device to receive push notifications for: - **Invoice validated** — e-invoice provider accepted your invoice - **Invoice rejected** — e-invoice provider rejected your invoice (with error details) - **Invoice overdue** — past due date - **Payment received** — payment recorded on an invoice - **Invoice issued** — new invoice finalized Notification preferences are configurable per channel (push, email, in-app) from the settings screen. ## E-Invoice Provider Integration The app provides full e-invoice provider integration: - View provider token status and expiry - Trigger manual sync with the e-invoice provider - Monitor sync progress - View e-invoice messages - Submit invoices to the provider - Track validation and rejection statuses ## Company Registry Lookup Look up Romanian companies by CIF directly from the app. The registry lookup returns: - Company name and legal form - Registered address - VAT registration status - ANAF fiscal attributes This is useful when adding new clients — enter their CIF and the app auto-fills their details. ## Tech Stack Built with React Native and Expo for cross-platform compatibility: - **React Native 0.81** with React 19 - **Expo 54** with file-based routing (Expo Router) - **React Query** for data fetching and caching - **Zustand** for state management - **Sentry** for error tracking - **i18next** for internationalization --- ## Stripe App > Automatically create e-invoices from Stripe payments using the Storno.ro Stripe marketplace app. URL: https://docs.storno.ro/integrations/stripe-app # Stripe App The Storno.ro Stripe app brings e-invoicing directly into your Stripe dashboard. Primarily designed for Romanian e-Factura compliance, it automatically creates e-invoices from Stripe payments and submits them to the e-invoice provider, eliminating manual data entry. Available on the [Stripe App Marketplace](https://marketplace.stripe.com/apps/storno). ## Features - **Automatic e-invoice creation** from Stripe invoices, payments, and subscriptions - **Storno reversals** from Stripe refunds — reverse the original invoice with negative quantities (partial refunds reverse proportionally) - **Customer matching** — links Stripe customers to Storno.ro clients by CIF, email, or name - **Provider submission** — submit invoices to the e-invoice provider directly from the Stripe dashboard - **Status tracking** — monitor invoice lifecycle (draft, issued, sent to provider, validated, rejected) - **Error recovery** — retry rejected invoices with one click - **Auto mode** — fully automated invoice creation and provider submission ## Dashboard Views ### Home Overview The main dashboard shows your invoice pipeline at a glance: - Status counts: validated, in progress, rejected, issued, draft - Auto mode toggle - Tabbed filtering: All, In progress, Errors - Recent invoices with amounts and status ### Customer Detail When viewing a Stripe customer, the app shows: - Matched Storno.ro client details (name, CIF, email, address) - Customer's invoice history from Storno.ro - Matching is automatic based on Romanian tax IDs (CIF), EU VAT numbers, email, or name ### Payment Detail When viewing a Stripe payment, the app shows: - Payment amount and status - Linked e-invoices with status visualization - **Create e-invoice** button if no invoice exists yet - **Retry at provider** button for rejected invoices - Provider error details for failed submissions - A **Refunds** section listing every refund on the payment. For each refund it shows the linked storno invoice, or a **Create storno invoice** button if none exists yet (see [Storno reversal flow](#storno-reversal-flow)) ### Stripe Invoice Detail When viewing a Stripe invoice, the app shows: - Invoice amount and status - Linked Storno.ro e-invoice with full status pipeline visualization (Draft → Issued → Sent to provider → Validated) - **Create e-invoice** button if no linked Storno.ro invoice exists yet - **Retry at provider** button for rejected invoices > Refunds are handled from the **Payment Detail** view's Refunds section — Stripe has no dedicated refund page, so the app surfaces refund reversals on the payment. ### Subscription Detail When viewing a Stripe subscription, the app shows: - Subscription plan, amount, interval, and status - A list of all billing cycles (Stripe invoices), each showing the period, amount, Stripe invoice status, and linked Storno.ro invoice status - **Create e-invoice** button per cycle for cycles that have not yet been invoiced ## Settings The settings panel shows: - Connection status badge (connected / disconnected) - Connected company name and CIF - User who authorized the connection and when - Auto mode toggle - Disconnect button with confirmation step ## Setup ### 1. Install the app Install the Storno.ro app from the [Stripe App Marketplace](https://marketplace.stripe.com/apps/storno). ### 2. Connect your account 1. Open the app settings in your Stripe dashboard 2. Click **Connect Storno.ro** — the app initiates an OAuth 2.0 device authorization flow and shows a short code with an **Open Storno.ro to authorize** link 3. Click the link to open the Storno.ro consent page, sign in if needed, select the company, and click **Authorize** 4. The app polls Storno.ro and finishes the connection automatically once you approve 5. Authentication tokens are stored securely in Stripe's per-user secret store ### 3. Select a company During the consent step you choose which Storno.ro company to authorize. The dropdown shows every company accessible to you under the active organization. The authorization is scoped to that single company — to switch to a different company, disconnect and reconnect from the Stripe extension settings. ### 4. Enable auto mode (optional) Toggle auto mode to automatically create e-invoices and submit them to the e-invoice provider whenever Stripe finalizes an invoice. When disabled, you can create invoices manually from the payment detail view. ## How It Works ### Customer Matching The app matches Stripe customers to Storno.ro clients using: 1. **Romanian tax IDs** — extracts CIF from Stripe's `tax_ids` field (types: `ro_tin`, `eu_vat` with RO prefix) 2. **Email address** — matches by customer email 3. **Name** — falls back to name matching When a match is found, the app displays the full client details and invoice history. If no match is found, you can create the client in Storno.ro first. ### Invoice Creation Flow 1. Stripe finalizes an invoice (the `invoice.finalized` webhook) 2. If auto mode is on, the app automatically: - Finds the matching Storno.ro client - Creates a draft invoice with the line items from Stripe - Issues the invoice (generates XML and PDF) - Submits to the e-invoice provider 3. When Stripe marks the invoice paid (the `invoice.paid` webhook), the payment is recorded against the e-invoice 4. If auto mode is off, you can trigger creation manually from the payment detail view, the Stripe invoice detail view, or per billing cycle in the subscription detail view ### Storno Reversal Flow When a Stripe refund exists for a payment that has a linked Storno.ro invoice, the app can issue a **storno invoice** (factura de storno) — a reversal of the original invoice rather than a separate credit-note document: 1. Issue the refund in your Stripe dashboard, then open the payment's detail page 2. In the app's **Refunds** section, each refund shows its linked storno invoice (if any) 3. If none exists, click **Create storno invoice** — the app resolves the refund → charge → Stripe invoice → parent Storno.ro invoice chain and reverses the original invoice 4. The storno keeps the original invoice's series, document type, and per-line VAT rates, with **negated quantities**. A full refund reverses the whole invoice; a partial refund reverses proportionally 5. The storno is then submitted to the e-invoice provider following the same status pipeline as regular invoices If the original payment has no parent e-invoice in Storno.ro, the app explains that the e-invoice must be created first. ### Status Pipeline Invoices and storno reversals progress through these statuses: ``` Draft → Issued → Sent to provider → Validated → Rejected (retry available) ``` The app shows the current status with color-coded badges and provides retry functionality for rejected invoices. ## Authentication The app uses the OAuth 2.0 Device Authorization Grant ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)): 1. The app calls `POST /api/v1/stripe-app/oauth/device` to receive a `device_code` (long, opaque, polled by the app) and a `user_code` (short, shown to the user) 2. The app displays a link to `https://app.storno.ro/stripe-link?code={user_code}` that the user clicks to open the consent page 3. The user signs in to Storno.ro, selects which company to authorize, and approves the request via `POST /api/v1/stripe-app/oauth/approve` (body: `user_code`, `company_id`, `approve: true`). The server verifies that the user has access to the requested company before recording the grant. 4. The app polls `POST /api/v1/stripe-app/token` with `grant_type=device_code` until it receives access and refresh tokens 5. Tokens are stored in Stripe's encrypted secret store (scoped per user) 6. Tokens auto-refresh on expiration 7. On 401 responses, the app automatically clears tokens and prompts re-authentication ### Stripe Permissions The app requires these Stripe permissions: | Permission | Purpose | |------------|---------| | `customer_read` | Match Stripe customers with Storno.ro clients | | `payment_intent_read` | Display payment statuses and link payments to e-invoices | | `charge_read` | Read charge refunds to create storno reversals | | `invoice_read` | Create e-invoices from Stripe invoices | | `subscription_read` | View subscription billing cycles | | `secret_write` | Securely store authentication tokens | ## Disconnecting To disconnect your Storno.ro account, go to the app settings and click **Disconnect**. You will be asked to confirm before the action is taken. This revokes the authentication tokens and clears all stored credentials from Stripe's secret store. --- ## Bank Account > Bank account object for payment tracking URL: https://docs.storno.ro/objects/bank-account # Bank Account The BankAccount object represents a bank account associated with a company. Companies can have multiple bank accounts, with one marked as default for invoices. ## Attributes | Attribute | Type | Description | |-----------|------|-------------| | id | UUID | Unique identifier | | iban | string | International Bank Account Number | | bankName | string | Name of the bank | | currency | string | 3-letter currency code (e.g., "RON", "EUR", "USD") | | isDefault | boolean | Whether this is the default account for invoices | | source | string | Source: manual, anaf, import | | createdAt | datetime | Timestamp when created | | updatedAt | datetime | Timestamp of last update | | deletedAt | datetime | Soft delete timestamp (null if not deleted) | ## Example ```json { "id": "e1f2a3b4-c5d6-7890-5678-901234567890", "iban": "RO49BTRL01234567890123", "bankName": "Banca Transilvania", "currency": "RON", "isDefault": true, "source": "manual", "createdAt": "2024-01-05T10:00:00+02:00", "updatedAt": "2024-01-05T10:00:00+02:00", "deletedAt": null } ``` ## Multiple Accounts Example ```json [ { "id": "e1f2a3b4-c5d6-7890-5678-901234567890", "iban": "RO49BTRL01234567890123", "bankName": "Banca Transilvania", "currency": "RON", "isDefault": true, "source": "manual" }, { "id": "f2a3b4c5-d6e7-8901-6789-012345678901", "iban": "RO49BTRL09876543210987", "bankName": "Banca Transilvania", "currency": "EUR", "isDefault": false, "source": "manual" }, { "id": "a3b4c5d6-e7f8-9012-7890-123456789012", "iban": "RO49RNCB00820456789101", "bankName": "BCR", "currency": "RON", "isDefault": false, "source": "manual" } ] ``` ## Notes - Only one bank account per currency can be marked as `isDefault: true` - The default account is automatically selected when creating invoices - **iban**: Must be a valid IBAN format - **currency**: Typically matches the company's `defaultCurrency`, but multi-currency accounts are supported - **source**: - `manual` - Created by user - `anaf` - Imported from ANAF data - `import` - Bulk imported - Bank accounts can be soft-deleted (marked with `deletedAt`) but remain in database - When generating invoices, the system selects the appropriate bank account based on invoice currency --- ## Client > Client object representing companies and individuals URL: https://docs.storno.ro/objects/client # Client The Client object represents customers who receive invoices and other documents. Clients can be companies (legal entities) or individuals, and can be synced from e-invoice provider systems. ## Attributes | Attribute | Type | In List | In Detail | Description | |-----------|------|---------|-----------|-------------| | id | UUID | ✓ | ✓ | Unique identifier | | type | string | ✓ | ✓ | Client type: company or individual | | name | string | ✓ | ✓ | Company name or individual full name | | cui | string | ✓ | ✓ | CUI (Cod Unic de Identificare) for companies | | cnp | string | ✓ | ✓ | CNP (Cod Numeric Personal) for individuals | | vatCode | string | ✓ | ✓ | Full VAT code with country prefix (e.g., "RO12345678") | | isVatPayer | boolean | ✓ | ✓ | Whether the client is registered for VAT | | address | string | ✓ | ✓ | Street address | | city | string | ✓ | ✓ | City name | | email | string | ✓ | ✓ | Email address for invoices and communications | | contactPerson | string | ✓ | ✓ | Contact person name | | clientCode | string | ✓ | ✓ | Custom client code/reference | | registrationNumber | string | ✗ | ✓ | Company registration number (număr de înregistrare) | | county | string | ✗ | ✓ | County/state | | country | string | ✓ | ✓ | Country code (e.g., "RO", "DE") | | postalCode | string | ✗ | ✓ | Postal/ZIP code | | phone | string | ✗ | ✓ | Phone number | | bankName | string | ✗ | ✓ | Bank name for payments | | bankAccount | string | ✗ | ✓ | Bank account number (IBAN) | | defaultPaymentTermDays | integer | ✗ | ✓ | Default payment term in days | | notes | text | ✗ | ✓ | Internal notes about the client | | viesValid | boolean \| null | ✓ | ✓ | VIES validation result for EU clients (`true` = valid, `false` = invalid, `null` = not validated) | | viesValidatedAt | datetime \| null | ✗ | ✓ | Timestamp of last VIES validation | | viesName | string \| null | ✗ | ✓ | Company name as registered in the VIES system | | source | string | ✗ | ✓ | Source: manual, anaf, import | | lastSyncedAt | datetime | ✗ | ✓ | Last sync timestamp from e-invoice provider | | createdAt | datetime | ✓ | ✓ | Timestamp when created | | updatedAt | datetime | ✓ | ✓ | Timestamp of last update | | deletedAt | datetime | ✓ | ✓ | Soft delete timestamp (null if not deleted) | ## Example - Company Client ```json { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "type": "company", "name": "Acme Corporation SRL", "cui": "12345678", "cnp": null, "vatCode": "RO12345678", "isVatPayer": true, "address": "Strada Exemplu, nr. 10", "city": "Bucharest", "email": "billing@acme.ro", "contactPerson": "Ion Popescu", "clientCode": "ACME-001", "registrationNumber": "J40/1234/2020", "county": "Bucuresti", "country": "RO", "postalCode": "010101", "phone": "+40 21 123 4567", "bankName": "Banca Transilvania", "bankAccount": "RO49AAAA1B31007593840000", "defaultPaymentTermDays": 30, "notes": "VIP client - priority support", "source": "manual", "lastSyncedAt": null, "createdAt": "2024-01-15T10:00:00+02:00", "updatedAt": "2024-02-10T14:30:00+02:00", "deletedAt": null } ``` ## Example - Individual Client ```json { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "type": "individual", "name": "Maria Ionescu", "cui": null, "cnp": "2850123456789", "vatCode": null, "isVatPayer": false, "address": "Bulevardul Libertății, nr. 5, Ap. 12", "city": "Brașov", "email": "maria.ionescu@email.ro", "contactPerson": null, "clientCode": "IND-042", "registrationNumber": null, "county": "Brașov", "country": "RO", "postalCode": "500123", "phone": "+40 744 123 456", "bankName": null, "bankAccount": null, "defaultPaymentTermDays": 15, "notes": "Individual client - cash payments preferred", "source": "manual", "lastSyncedAt": null, "createdAt": "2024-02-01T11:00:00+02:00", "updatedAt": "2024-02-01T11:00:00+02:00", "deletedAt": null } ``` ## Notes - **type**: Use `company` for legal entities, `individual` for natural persons - **cui** vs **cnp**: Companies use CUI, individuals use CNP - **vatCode**: Full VAT code with country prefix (RO prefix for Romania) - **isVatPayer**: Determines whether VAT is applied on invoices - **source**: `manual` (user-created), `anaf` (synced from e-invoice provider), `import` (bulk import) - **viesValid**: Automatically set when a foreign EU client is created/updated with a VAT code. Used to determine reverse charge eligibility and OSS applicability. - Clients synced from an e-invoice provider have `lastSyncedAt` timestamp - Soft-deleted clients have `deletedAt` set but remain in database --- ## Company > Company object representing a business entity in the system URL: https://docs.storno.ro/objects/company # Company The Company object represents a business entity within the system. Each organization can have multiple companies. Companies are the main entities that issue invoices and interact with e-invoice provider systems. ## Attributes | Attribute | Type | Description | |-----------|------|-------------| | id | UUID | Unique identifier | | name | string | Company legal name | | cif | integer | CUI/CIF number (without country prefix) | | registrationNumber | string | Company registration number (număr de înregistrare) | | vatPayer | boolean | Whether the company is registered for VAT | | vatCode | string | Full VAT code with country prefix (e.g., "RO12345678") | | address | string | Street address | | city | string | City name | | state | string | County/state | | country | string | Country code (e.g., "RO") | | sector | string | Sector (for Bucharest) | | phone | string | Phone number | | email | string | Email address | | bankName | string | Default bank name | | bankAccount | string | Default bank account (IBAN) | | bankBic | string | Bank BIC/SWIFT code | | defaultCurrency | string | 3-letter default currency code (e.g., "RON") | | archiveEnabled | boolean | Whether automatic archiving is enabled | | archiveRetentionYears | integer | How many years to retain archived documents | | syncEnabled | boolean | Whether e-invoice sync is enabled | | lastSyncedAt | datetime | Last sync timestamp from e-invoice provider | | syncDaysBack | integer | How many days back to sync from e-invoice provider | | efacturaDelayHours | integer | Delay in hours before syncing from e-invoice provider | | createdAt | datetime | Timestamp when created | | updatedAt | datetime | Timestamp of last update | ## Example ```json { "id": "a7b8c9d0-e1f2-3456-1234-567890123456", "name": "Example Tech SRL", "cif": 12345678, "registrationNumber": "J40/1234/2020", "vatPayer": true, "vatCode": "RO12345678", "address": "Strada Principală, nr. 42", "city": "Bucharest", "state": "Bucuresti", "country": "RO", "sector": "Sector 1", "phone": "+40 21 123 4567", "email": "contact@exampletech.ro", "bankName": "Banca Transilvania", "bankAccount": "RO49BTRL01234567890123", "bankBic": "BTRLRO22", "defaultCurrency": "RON", "archiveEnabled": true, "archiveRetentionYears": 10, "syncEnabled": true, "lastSyncedAt": "2024-02-16T08:00:00+02:00", "syncDaysBack": 30, "efacturaDelayHours": 2, "createdAt": "2024-01-01T10:00:00+02:00", "updatedAt": "2024-02-16T08:00:00+02:00" } ``` ## E-Invoice Sync Settings - **syncEnabled**: Master switch for e-invoice provider synchronization - **syncDaysBack**: Number of days to look back when syncing invoices - **efacturaDelayHours**: Delay before syncing to allow for provider processing - **lastSyncedAt**: Tracks when the last successful sync occurred ## Archive Settings - **archiveEnabled**: Enables automatic document archiving - **archiveRetentionYears**: How long to keep documents before deletion (typically 10 years for legal compliance) ## Notes - Each company requires provider-specific credentials to use e-invoice sync (e.g., ANAF OAuth for Romania) - The **cif** field stores the numeric CUI/CIF without country prefix - The **vatCode** field includes the country prefix (e.g., "RO12345678") - Companies belong to organizations and users access them through memberships - Multiple users can have access to the same company with different permission levels --- ## Credit Note > Credit note object for invoice corrections and refunds URL: https://docs.storno.ro/objects/credit-note # Credit Note Credit notes are represented using the Invoice entity with `isCreditNote: true` and `direction: outgoing`. They are used to correct or cancel previously issued invoices, providing a mechanism for refunds, corrections, or cancellations while maintaining audit compliance. Credit notes reference their parent invoice through the `parentDocument` field and are synced to the e-invoice provider like regular invoices. ## Attributes Credit notes use the same attributes as the [Invoice object](/objects/invoice), with these specific characteristics: - **isCreditNote**: Always `true` - **direction**: Always `outgoing` (even if correcting an incoming invoice) - **parentDocument**: Reference to the original invoice being credited - **total**: Typically negative or zero (representing the refund amount) - **lines**: Line items typically have negative quantities or prices All other attributes (status, currency, dates, client, e-invoice sync fields, etc.) function identically to regular invoices. ## Workflow 1. **Creation**: Credit note is created referencing a parent invoice 2. **Validation**: Lines and amounts are validated against parent invoice 3. **Sync**: Credit note is synced to the e-invoice provider like a regular invoice 4. **Application**: The credit amount is applied to the parent invoice balance ## Example ```json { "id": "b8c9d0e1-f2a3-4567-2345-678901234567", "number": "CN-2024-001", "status": "validated", "direction": "outgoing", "currency": "RON", "issueDate": "2024-02-20", "dueDate": "2024-02-20", "subtotal": -500.00, "vatTotal": -95.00, "total": -595.00, "clientName": "Acme Corporation SRL", "amountPaid": 0.00, "balance": -595.00, "client": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "type": "company", "name": "Acme Corporation SRL", "cui": "12345678", "vatCode": "RO12345678", "isVatPayer": true, "address": "Strada Exemplu, nr. 10", "city": "Bucharest", "email": "contact@acme.ro" }, "documentSeries": "CN", "isCreditNote": true, "isIncoming": false, "parentDocument": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "number": "INV-2024-001", "total": 1190.00 }, "notes": "Credit note for partial return of goods - Invoice INV-2024-001", "invoiceTypeCode": "standard", "anafUploadId": "4567890124", "anafUploadDate": "2024-02-20T14:00:00+02:00", "anafState": "validated", "pdfPath": "/storage/credit-notes/2024/02/CN-2024-001.pdf", "lines": [ { "id": "c9d0e1f2-a3b4-5678-3456-789012345678", "position": 1, "description": "Web Development Services (partial credit)", "quantity": -20.0, "unitOfMeasure": "hour", "unitPrice": 25.00, "vatRate": 19.00, "vatAmount": -95.00, "lineTotal": -595.00, "discount": 0.00 } ], "createdAt": "2024-02-20T13:00:00+02:00", "updatedAt": "2024-02-20T14:00:00+02:00", "deletedAt": null } ``` ## Notes - Credit notes must reference a valid parent invoice - The total is typically negative, reducing the parent invoice balance - Credit notes follow the same e-invoice sync workflow as invoices - Status progression: draft → synced → issued → sent_to_provider → validated - Credit notes appear in the same invoice list with `isCreditNote: true` filter --- ## Delivery Note > Delivery note object for goods shipment documentation URL: https://docs.storno.ro/objects/delivery-note # Delivery Note The DeliveryNote object represents shipping documents that accompany goods deliveries. Delivery notes can be issued for shipments and optionally converted into invoices later. ## Attributes | Attribute | Type | In List | In Detail | Description | |-----------|------|---------|-----------|-------------| | id | UUID | ✓ | ✓ | Unique identifier | | number | string | ✓ | ✓ | Delivery note number (e.g., "DN-2024-001") | | status | DeliveryNoteStatus | ✓ | ✓ | Status: draft, issued, converted, cancelled | | currency | string | ✓ | ✓ | 3-letter currency code (e.g., "RON", "EUR") | | issueDate | date | ✓ | ✓ | Date the delivery note was issued (YYYY-MM-DD) | | dueDate | date | ✓ | ✓ | Expected delivery date (YYYY-MM-DD) | | subtotal | decimal | ✓ | ✓ | Subtotal before VAT | | vatTotal | decimal | ✓ | ✓ | Total VAT amount | | total | decimal | ✓ | ✓ | Total amount including VAT | | clientName | string | ✓ | ✓ | Virtual field: name of the client | | client | Client | ✗ | ✓ | Full Client object | | documentSeriesId | UUID | ✗ | ✓ | UUID of the assigned document series. Auto-assigned from the default `delivery_note` series if not specified at creation | | documentSeries | string | ✗ | ✓ | Document series prefix | | discount | decimal | ✗ | ✓ | Total discount amount | | notes | text | ✗ | ✓ | Notes about the delivery | | mentions | text | ✗ | ✓ | Additional mentions on delivery note | | internalNote | text | ✗ | ✓ | Internal notes (not visible to client) | | deliveryLocation | string | ✗ | ✓ | Delivery address | | projectReference | string | ✗ | ✓ | Project or reference number | | issuerName | string | ✗ | ✓ | Name of the person issuing the delivery note | | issuerId | string | ✗ | ✓ | ID/CNP of the issuer | | salesAgent | string | ✗ | ✓ | Sales agent name | | deputyName | string | ✗ | ✓ | Deputy/driver name | | deputyIdentityCard | string | ✗ | ✓ | Deputy ID card number | | deputyAuto | string | ✗ | ✓ | Deputy vehicle registration | | exchangeRate | decimal | ✗ | ✓ | Exchange rate used (for foreign currencies) | | convertedInvoice | object | ✗ | ✓ | Reference to converted invoice (if status is converted) | | issuedAt | datetime | ✗ | ✓ | Timestamp when issued | | cancelledAt | datetime | ✗ | ✓ | Timestamp when cancelled | | lines | array | ✗ | ✓ | Array of DeliveryNoteLine objects | | etransportOperationType | integer | ✗ | ✓ | e-Transport operation type code (30=TTN domestic) | | etransportVehicleNumber | string | ✗ | ✓ | Vehicle registration number for transport | | etransportTrailer1 | string | ✗ | ✓ | First trailer registration number | | etransportTrailer2 | string | ✗ | ✓ | Second trailer registration number | | etransportTransporterCountry | string | ✗ | ✓ | Transporter country code (ISO 3166-1 alpha-2) | | etransportTransporterCode | string | ✗ | ✓ | Transporter CUI/CIF code | | etransportTransporterName | string | ✗ | ✓ | Transporter company name | | etransportTransportDate | date | ✗ | ✓ | Planned transport date (YYYY-MM-DD) | | etransportStartCounty | integer | ✗ | ✓ | Route start county code (Romanian county) | | etransportStartLocality | string | ✗ | ✓ | Route start locality name | | etransportStartStreet | string | ✗ | ✓ | Route start street name | | etransportStartNumber | string | ✗ | ✓ | Route start street number | | etransportStartOtherInfo | string | ✗ | ✓ | Route start additional info | | etransportStartPostalCode | string | ✗ | ✓ | Route start postal code | | etransportEndCounty | integer | ✗ | ✓ | Route end county code | | etransportEndLocality | string | ✗ | ✓ | Route end locality name | | etransportEndStreet | string | ✗ | ✓ | Route end street name | | etransportEndNumber | string | ✗ | ✓ | Route end street number | | etransportEndOtherInfo | string | ✗ | ✓ | Route end additional info | | etransportEndPostalCode | string | ✗ | ✓ | Route end postal code | | etransportUit | string | ✓ | ✓ | e-Transport UIT (unique identifier from ANAF) | | etransportStatus | string | ✓ | ✓ | e-Transport status: uploaded, ok, nok, validation_failed, upload_failed | | etransportErrorMessage | text | ✗ | ✓ | e-Transport error message from ANAF | | etransportSubmittedAt | datetime | ✗ | ✓ | Timestamp when submitted to e-Transport | | createdAt | datetime | ✓ | ✓ | Timestamp when created | | updatedAt | datetime | ✓ | ✓ | Timestamp of last update | | deletedAt | datetime | ✓ | ✓ | Soft delete timestamp (null if not deleted) | ## DeliveryNoteLine Attributes | Attribute | Type | In List | In Detail | Description | |-----------|------|---------|-----------|-------------| | id | UUID | ✗ | ✓ | Unique identifier | | position | integer | ✗ | ✓ | Line position order | | description | string | ✗ | ✓ | Product or service description | | quantity | decimal | ✗ | ✓ | Quantity | | unitOfMeasure | string | ✗ | ✓ | Unit of measure label (e.g., "box", "kg") | | unitPrice | decimal | ✗ | ✓ | Unit price | | vatRate | decimal | ✗ | ✓ | VAT rate percentage | | vatAmount | decimal | ✗ | ✓ | VAT amount | | lineTotal | decimal | ✗ | ✓ | Total line amount including VAT | | discount | decimal | ✗ | ✓ | Discount amount | | productCode | string | ✗ | ✓ | Product code or SKU | | tariffCode | string | ✗ | ✓ | Customs tariff code (4-8 digits) | | purposeCode | integer | ✗ | ✓ | Purpose code for TTN (101=Commerce, 704=Transfer, 705=Client stock, 9901=Other) | | unitOfMeasureCode | string | ✗ | ✓ | UN/ECE unit of measure code (e.g., KGM, LTR, MTR) | | netWeight | decimal | ✗ | ✓ | Net weight in kg | | grossWeight | decimal | ✗ | ✓ | Gross weight in kg | | valueWithoutVat | decimal | ✗ | ✓ | Value without VAT in RON | ## Example ```json { "id": "d0e1f2a3-b4c5-6789-4567-890123456789", "number": "DN-2024-001", "status": "issued", "currency": "RON", "issueDate": "2024-02-18", "dueDate": "2024-02-20", "subtotal": 1500.00, "vatTotal": 285.00, "total": 1785.00, "clientName": "Distribution SRL", "client": { "id": "e1f2a3b4-c5d6-7890-5678-901234567890", "type": "company", "name": "Distribution SRL", "cui": "34567890", "vatCode": "RO34567890", "isVatPayer": true, "address": "Strada Transport, nr. 15", "city": "Timisoara", "email": "logistics@distribution.ro" }, "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "documentSeries": "DN", "discount": 0.00, "notes": "Handle with care - fragile items", "mentions": "Driver will collect signature upon delivery", "internalNote": "Priority delivery", "deliveryLocation": "Strada Transport, nr. 15, Timisoara", "projectReference": "PROJ-2024-008", "issuerName": "Ion Popescu", "issuerId": "1850123456789", "salesAgent": "Maria Ionescu", "deputyName": "Vasile Georgescu", "deputyIdentityCard": "AB123456", "deputyAuto": "B-123-XYZ", "exchangeRate": 1.0000, "convertedInvoice": null, "issuedAt": "2024-02-18T08:00:00+02:00", "cancelledAt": null, "etransportOperationType": 30, "etransportVehicleNumber": "B-123-XYZ", "etransportTrailer1": null, "etransportTrailer2": null, "etransportTransporterCountry": "RO", "etransportTransporterCode": "12345678", "etransportTransporterName": "Transport SRL", "etransportTransportDate": "2024-02-20", "etransportStartCounty": 40, "etransportStartLocality": "Bucuresti", "etransportStartStreet": "Strada Industriilor", "etransportStartNumber": "10", "etransportStartOtherInfo": null, "etransportStartPostalCode": "010000", "etransportEndCounty": 35, "etransportEndLocality": "Timisoara", "etransportEndStreet": "Strada Transport", "etransportEndNumber": "15", "etransportEndOtherInfo": null, "etransportEndPostalCode": "300000", "etransportUit": "RO240218ABC123", "etransportStatus": "ok", "etransportErrorMessage": null, "etransportSubmittedAt": "2024-02-18T08:05:00+02:00", "lines": [ { "id": "f2a3b4c5-d6e7-8901-6789-012345678901", "position": 1, "description": "Product A - Box of 50", "quantity": 10.0, "unitOfMeasure": "box", "unitPrice": 150.00, "vatRate": 19.00, "vatAmount": 285.00, "lineTotal": 1785.00, "discount": 0.00, "productCode": "PROD-A-50", "tariffCode": "39269097", "purposeCode": 101, "unitOfMeasureCode": "KGM", "netWeight": 25.00, "grossWeight": 27.50, "valueWithoutVat": 1500.00 } ], "createdAt": "2024-02-18T07:00:00+02:00", "updatedAt": "2024-02-18T08:00:00+02:00", "deletedAt": null } ``` ## Workflow 1. **Draft**: Delivery note is created with line items 2. **Issued**: Delivery note is issued and accompanies the goods 3. **e-Transport Submitted** (optional): After issuing, the delivery note can be submitted to ANAF's e-Transport system — status transitions to `uploaded`, then `ok` (UIT received) or `nok` (rejected) 4. **Converted**: Optionally converted to an invoice after delivery 5. **Cancelled**: Can be cancelled if delivery doesn't occur ## e-Transport Integration e-Transport is Romania's ANAF system for declaring goods transported on national roads. Companies that meet the legal threshold must declare transport movements before the goods depart. ### How it works - After a delivery note is issued, it can be submitted to e-Transport via the [Submit Delivery Note to e-Transport](/api-reference/delivery-notes/submit-etransport) endpoint - The submission sends the transport declaration to ANAF and sets `etransportStatus` to `uploaded` - ANAF processes the declaration asynchronously: if accepted, `etransportStatus` becomes `ok` and the `etransportUit` field is populated with the UIT; if rejected, the status becomes `nok` and `etransportErrorMessage` contains the rejection reason - Validation failures before submission result in a `validation_failed` status; network or connectivity issues result in `upload_failed` ### UIT (Unique Identifier of Transport) The UIT is the transport's official tracking number issued by ANAF upon successful declaration. It must accompany the goods during transport and can be checked by authorities. The UIT is stored in the `etransportUit` field. ### Status flow ``` (not submitted) → uploaded → ok (UIT received, transport approved) → nok (rejected by ANAF, see etransportErrorMessage) → validation_failed (data failed validation before submission) → upload_failed (submission failed due to connectivity issues) ``` ### Line item fields for e-Transport Each delivery note line supports additional e-Transport-specific fields: `tariffCode` (customs tariff code), `purposeCode` (reason for transport), `unitOfMeasureCode` (UN/ECE code), `netWeight`, `grossWeight`, and `valueWithoutVat`. These are required by ANAF for a valid transport declaration. ## Notes - Delivery notes track the physical movement of goods - They can be converted to invoices after successful delivery - Deputy fields track the person transporting the goods - Delivery location can differ from client's registered address - PDFs are generated using the company's [PDF template configuration](/objects/pdf-template-config) --- ## Document Series > Document series object for sequential numbering URL: https://docs.storno.ro/objects/document-series # Document Series The DocumentSeries object represents a numbering series for documents (invoices, proformas, credit notes, delivery notes). Each series has a prefix and maintains a sequential counter. ## Attributes | Attribute | Type | Description | |-----------|------|-------------| | id | UUID | Unique identifier | | prefix | string | Series prefix (e.g., "INV", "PRO", "CN", "DN") | | currentNumber | integer | Current sequential number | | type | string | Document type: invoice, proforma, credit_note, delivery_note | | active | boolean | Whether this series is active and available for use | | source | string | Source: manual, anaf, import | | nextNumber | string | Virtual field: formatted next number (prefix + padded number) | | createdAt | datetime | Timestamp when created | | updatedAt | datetime | Timestamp of last update | | deletedAt | datetime | Soft delete timestamp (null if not deleted) | ## Example ```json { "id": "f2a3b4c5-d6e7-8901-6789-012345678901", "prefix": "INV", "currentNumber": 125, "type": "invoice", "active": true, "source": "manual", "nextNumber": "INV-0126", "createdAt": "2024-01-01T10:00:00+02:00", "updatedAt": "2024-02-15T16:45:00+02:00", "deletedAt": null } ``` ## Multiple Series Example ```json [ { "id": "f2a3b4c5-d6e7-8901-6789-012345678901", "prefix": "INV", "currentNumber": 125, "type": "invoice", "active": true, "nextNumber": "INV-0126" }, { "id": "a3b4c5d6-e7f8-9012-7890-123456789012", "prefix": "PRO", "currentNumber": 42, "type": "proforma", "active": true, "nextNumber": "PRO-0043" }, { "id": "b4c5d6e7-f8a9-0123-8901-234567890123", "prefix": "CN", "currentNumber": 8, "type": "credit_note", "active": true, "nextNumber": "CN-0009" }, { "id": "c5d6e7f8-a9b0-1234-9012-345678901234", "prefix": "DN", "currentNumber": 15, "type": "delivery_note", "active": true, "nextNumber": "DN-0016" }, { "id": "d6e7f8a9-b0c1-2345-0123-456789012345", "prefix": "2023-INV", "currentNumber": 450, "type": "invoice", "active": false, "nextNumber": "2023-INV-0451" } ] ``` ## Document Types - **invoice**: Regular invoices - **proforma**: Proforma invoices (quotations) - **credit_note**: Credit notes - **delivery_note**: Delivery notes ## Number Formatting The `nextNumber` virtual field formats the next document number: - Combines `prefix` with `currentNumber + 1` - Numbers are zero-padded to 4 digits minimum - Examples: - `INV` + 125 → `INV-0126` - `PRO` + 5 → `PRO-0006` - `2024-INV` + 1234 → `2024-INV-1235` ## Common Series Patterns ### By Year - `2024-INV`, `2024-PRO`, `2024-CN` - Reset numbering each year ### By Type - `INV` - Regular invoices - `PRO` - Proformas - `CN` - Credit notes - `DN` - Delivery notes ### By Purpose - `INV-EXP` - Export invoices - `INV-RO` - Domestic invoices - `INV-EU` - EU invoices ## Notes - Only one series per type should be `active: true` at a time (or users select from multiple) - The `currentNumber` increments with each issued document - Inactive series (previous years) remain in system with `active: false` - **source**: - `manual` - Created by user - `anaf` - From ANAF configuration - `import` - Bulk imported - Series can be soft-deleted but numbers cannot be reused - The system automatically locks and increments `currentNumber` to prevent duplicate numbers --- ## Email Template > Email template object for automated communications URL: https://docs.storno.ro/objects/email-template # Email Template The EmailTemplate object represents a reusable email template for sending invoices, proformas, and other documents to clients. Templates support variable substitution for personalization. ## Attributes | Attribute | Type | Description | |-----------|------|-------------| | id | UUID | Unique identifier | | name | string | Template name (internal reference) | | subject | string | Email subject line (supports variables) | | body | text | Email body content (HTML or plain text, supports variables) | | isDefault | boolean | Whether this is the default template for its type | | createdAt | datetime | Timestamp when created | | updatedAt | datetime | Timestamp of last update | | deletedAt | datetime | Soft delete timestamp (null if not deleted) | ## Available Variables Templates support the following variable substitution: ### Company Variables - `{{company.name}}` - Company name - `{{company.cif}}` - Company CIF/CUI - `{{company.address}}` - Company address - `{{company.phone}}` - Company phone - `{{company.email}}` - Company email ### Client Variables - `{{client.name}}` - Client name - `{{client.contactPerson}}` - Contact person name - `{{client.email}}` - Client email ### Document Variables - `{{document.number}}` - Document number (e.g., "INV-2024-001") - `{{document.issueDate}}` - Issue date - `{{document.dueDate}}` - Due date - `{{document.total}}` - Total amount - `{{document.currency}}` - Currency code - `{{document.downloadUrl}}` - PDF download link ### Other Variables - `{{year}}` - Current year - `{{date}}` - Current date ## Example - Invoice Email Template ```json { "id": "b4c5d6e7-f8a9-0123-8901-234567890123", "name": "Invoice Email - Standard", "subject": "Invoice {{document.number}} from {{company.name}}", "body": "\n\n\n \n\n\n

Bună ziua {{client.contactPerson}},

\n \n

Vă transmitem în atașament factura {{document.number}} în valoare de {{document.total}} {{document.currency}}.

\n \n

Detalii factură:

\n \n \n

Puteți descărca factura aici: Download PDF

\n \n

Vă mulțumim pentru colaborare!

\n \n

Cu stimă,
\n {{company.name}}
\n {{company.phone}}
\n {{company.email}}

\n\n", "isDefault": true, "createdAt": "2024-01-01T10:00:00+02:00", "updatedAt": "2024-01-15T14:30:00+02:00", "deletedAt": null } ``` ## Example - Proforma Email Template ```json { "id": "c5d6e7f8-a9b0-1234-9012-345678901234", "name": "Proforma Email - Standard", "subject": "Proforma {{document.number}} - {{company.name}}", "body": "\n\n\n

Bună ziua {{client.contactPerson}},

\n \n

Vă transmitem oferta noastră nr. {{document.number}}.

\n \n

Detalii proformă:

\n \n \n

Puteți descărca proforma aici: Download PDF

\n \n

Rămânem la dispoziția dumneavoastră pentru orice clarificări.

\n \n

Cu stimă,
\n {{company.name}}

\n\n", "isDefault": true, "createdAt": "2024-01-01T10:00:00+02:00", "updatedAt": "2024-01-01T10:00:00+02:00", "deletedAt": null } ``` ## Example - Payment Reminder Template ```json { "id": "d6e7f8a9-b0c1-2345-0123-456789012345", "name": "Payment Reminder", "subject": "Reminder: Invoice {{document.number}} - Payment Due", "body": "

Bună ziua {{client.contactPerson}},

\n\n

Vă reamintim că factura {{document.number}} în valoare de {{document.total}} {{document.currency}} ajunge la scadență pe data de {{document.dueDate}}.

\n\n

Dacă ați efectuat deja plata, vă rugăm să ignorați acest mesaj.

\n\n

Cu stimă,
{{company.name}}

", "isDefault": false, "createdAt": "2024-01-05T11:00:00+02:00", "updatedAt": "2024-01-05T11:00:00+02:00", "deletedAt": null } ``` ## Notes - Templates support both **HTML** and **plain text** formatting - Variables are replaced at send time with actual document data - **isDefault**: Only one template per document type should be default - Templates can include inline CSS for email styling - The system validates that all variables used in the template are valid - Attachments (PDF invoices) are automatically added when sending - Templates can be duplicated and customized for specific clients or scenarios - Soft-deleted templates remain in database but are hidden from selection --- ## Enums > Enumeration types used throughout the API URL: https://docs.storno.ro/objects/enums # Enums This document lists all enumeration types used in the Storno.ro API. ## DocumentStatus Status values for invoices and other documents: | Value | Description | |-------|-------------| | draft | Document is in draft state, not yet finalized | | synced | Document synced from e-invoice provider but not yet issued | | issued | Document has been issued to the client | | sent_to_provider | Document has been sent to e-invoice provider | | validated | Document validated by e-invoice provider | | rejected | Document rejected by e-invoice provider | | cancelled | Document has been cancelled | | paid | Invoice fully paid | | partially_paid | Invoice partially paid | | overdue | Invoice past due date and not fully paid | | converted | Document converted to another type (e.g., proforma → invoice) | ## EInvoiceSubmissionStatus Status of an e-invoice submission to a provider (ANAF, SDI, KSeF, etc.): | Value | Description | |-------|-------------| | pending | Submission is queued and waiting to be sent | | submitted | Successfully submitted to the provider, awaiting response | | accepted | Provider accepted the e-invoice | | rejected | Provider rejected the e-invoice | | error | An error occurred during submission | ## DocumentType Types of documents: | Value | Description | |-------|-------------| | invoice | Regular invoice | | credit_note | Credit note (invoice correction) | ## InvoiceDirection Direction of invoice: | Value | Description | |-------|-------------| | incoming | Incoming invoice (received from supplier) | | outgoing | Outgoing invoice (issued to client) | ## ProformaStatus Status values specific to proforma invoices: | Value | Description | |-------|-------------| | draft | Proforma in draft state | | sent | Proforma sent to client | | accepted | Proforma accepted by client | | rejected | Proforma rejected by client | | converted | Proforma converted to invoice | | cancelled | Proforma cancelled | ## DeliveryNoteStatus Status values for delivery notes: | Value | Description | |-------|-------------| | draft | Delivery note in draft state | | issued | Delivery note issued and sent with goods | | converted | Delivery note converted to invoice | | cancelled | Delivery note cancelled | ## OrganizationRole User roles within an organization: | Value | Description | Typical Permissions | |-------|-------------|---------------------| | owner | Organization owner | Full access to everything | | admin | Administrator | Can manage users, settings, documents | | accountant | Accountant | Can view/manage financial documents, limited settings access | | employee | Employee | Limited access based on assigned permissions | ## InvoiceTypeCode Invoice type codes for e-invoice submission: | Value | Description (English) | Description (Romanian) | |-------|----------------------|------------------------| | standard | Standard invoice | Factură standard | | reverse_charge | Reverse charge | Taxare inversă | | exempt_with_deduction | Exempt with deduction right | Scutit cu drept de deducere | | services_art_311 | Services Art. 311 | Servicii Art. 311 | | sales_art_312 | Sales Art. 312 | Vânzări Art. 312 | | non_taxable | Non-taxable | Neimpozabil | | special_regime_art_314_315 | Special regime Art. 314/315 | Regim special Art. 314/315 | | non_transfer | Non-transfer | Netransfer | | simplified | Simplified invoice | Factură simplificată | | services_art_278 | Services Art. 278 | Servicii Art. 278 | | exempt_art_294_ab | Exempt Art. 294 (a-b) | Scutit Art. 294 (a-b) | | exempt_art_294_cd | Exempt Art. 294 (c-d) | Scutit Art. 294 (c-d) | | self_billing | Self-billing | Autofacturare | ## EmailStatus Status of sent emails: | Value | Description | |-------|-------------| | sent | Email successfully sent | | delivered | Email delivered to recipient | | bounced | Email bounced (invalid address) | | failed | Email failed to send | ## RecurringFrequency Frequency options for recurring invoices: | Value | Description | |-------|-------------| | monthly | Every month | | quarterly | Every 3 months | | yearly | Once per year | ## DueDateType How to calculate due date for recurring invoices: | Value | Description | |-------|-------------| | days_after | X days after issue date | | fixed_day | Fixed day of the month | ## PriceRule Dynamic pricing rules for recurring invoice lines: | Value | Description | |-------|-------------| | fixed | Use the template price (no changes) | | latest_product_price | Use current product price at generation time | | apply_exchange_rate | Convert from reference currency using latest rate | ## ClientType Type of client: | Value | Description | |-------|-------------| | company | Legal entity (company) | | individual | Natural person (individual) | ## Source Data source for imported/synced entities: | Value | Description | |-------|-------------| | manual | Manually created by user | | anaf | Synced from e-invoice provider (ANAF in Romania) | | import | Bulk imported (CSV, etc.) | ## PaymentMethod Payment methods for tracking invoice payments: | Value | Description | |-------|-------------| | bank_transfer | Bank transfer (Ordin de plată) | | cash | Cash payment (Numerar) | | card | Card payment (Card) | | check | Check (Cec) | | paypal | PayPal | | stripe | Stripe | | mobilpay | MobilPay | | netopia | Netopia Payments | | other | Other method | ## Notes - All enum values are **lowercase with underscores** (snake_case) - Frontend applications should map these to user-friendly labels - Romanian translations are provided in the i18n files (`ro.ts`) - Some enums have specific e-invoice provider requirements (InvoiceTypeCode, DocumentStatus) - Enum values are case-sensitive in API requests --- ## Invoice > Invoice object representing both incoming and outgoing invoices URL: https://docs.storno.ro/objects/invoice # Invoice The Invoice object represents both outgoing (issued by the company) and incoming (received from suppliers) invoices. Invoices can be synced with e-invoice provider systems and support multiple currencies, payment tracking, and credit notes. ## Attributes | Attribute | Type | In List | In Detail | Description | |-----------|------|---------|-----------|-------------| | id | UUID | ✓ | ✓ | Unique identifier | | number | string | ✓ | ✓ | Invoice number (e.g., "INV-2024-001") | | status | DocumentStatus | ✓ | ✓ | Current status (draft, issued, validated, paid, etc.) | | direction | InvoiceDirection | ✓ | ✓ | Direction: incoming or outgoing | | currency | string | ✓ | ✓ | 3-letter currency code (e.g., "RON", "EUR") | | language | string | ✗ | ✓ | Document language for PDF generation: `ro`, `en`, `de`, `fr` (default: `ro`) | | issueDate | date | ✓ | ✓ | Date the invoice was issued (YYYY-MM-DD) | | dueDate | date | ✓ | ✓ | Payment due date (YYYY-MM-DD) | | subtotal | decimal | ✓ | ✓ | Subtotal before VAT | | vatTotal | decimal | ✓ | ✓ | Total VAT amount | | total | decimal | ✓ | ✓ | Total amount including VAT | | clientName | string | ✓ | ✓ | Virtual field: name of the client | | amountPaid | decimal | ✓ | ✓ | Total amount paid to date | | balance | decimal | ✓ | ✓ | Virtual field: remaining balance (total - amountPaid) | | supplier | object | ✓ | ✓ | Supplier relation with id, name, cif (for incoming invoices) | | client | Client | ✗ | ✓ | Full Client object | | documentSeries | string | ✗ | ✓ | Document series prefix | | discount | decimal | ✗ | ✓ | Total discount amount | | notes | text | ✗ | ✓ | Public notes visible to client | | paymentTerms | text | ✗ | ✓ | Payment terms description | | invoiceTypeCode | InvoiceTypeCode | ✗ | ✓ | Type code for e-invoice submission (standard, reverse_charge, etc.) | | deliveryLocation | string | ✗ | ✓ | Delivery address | | projectReference | string | ✗ | ✓ | Project or reference number | | exchangeRate | decimal | ✗ | ✓ | Exchange rate used (for foreign currencies) | | anafUploadId | string | ✗ | ✓ | ANAF upload ID after submission (Romania only) | | anafUploadDate | datetime | ✗ | ✓ | Date/time uploaded to ANAF (Romania only) | | anafState | string | ✗ | ✓ | Current state in ANAF system (Romania only) | | anafDownloadId | string | ✗ | ✓ | ANAF download ID for incoming invoices (Romania only) | | anafXmlUrl | string | ✗ | ✓ | URL to the ANAF XML file (Romania only) | | pdfPath | string | ✗ | ✓ | Path to generated PDF file | | isIncoming | boolean | ✗ | ✓ | True if incoming invoice | | isCreditNote | boolean | ✗ | ✓ | True if this is a credit note | | collect | boolean | ✗ | ✓ | Collection flag | | parentDocument | object | ✗ | ✓ | Reference to parent invoice (for credit notes) | | orderNumber | string | ✗ | ✓ | Purchase order number | | contractNumber | string | ✗ | ✓ | Contract reference number | | issuerName | string | ✗ | ✓ | Name of the person issuing the invoice | | issuerId | string | ✗ | ✓ | ID/CNP of the issuer | | mentions | text | ✗ | ✓ | Additional mentions on invoice | | internalNote | text | ✗ | ✓ | Internal notes (not visible to client) | | salesAgent | string | ✗ | ✓ | Sales agent name | | deputyName | string | ✗ | ✓ | Deputy/representative name | | deputyIdentityCard | string | ✗ | ✓ | Deputy ID card number | | deputyAuto | string | ✗ | ✓ | Deputy vehicle registration | | penaltyEnabled | boolean | ✗ | ✓ | Whether late payment penalties are enabled | | penaltyPercentPerDay | decimal | ✗ | ✓ | Daily penalty percentage | | penaltyGraceDays | integer | ✗ | ✓ | Grace period before penalties apply | | ublExtensions | object | ✗ | ✓ | UBL extension fields (invoicePeriod, delivery, allowanceCharges, prepaidAmount, additionalDocumentReferences) | | lines | array | ✗ | ✓ | Array of InvoiceLine objects | | events | array | ✗ | ✓ | Array of DocumentEvent objects (audit trail) | | attachments | array | ✗ | ✓ | Array of file attachments | | payments | array | ✗ | ✓ | Array of Payment objects | | createdAt | datetime | ✓ | ✓ | Timestamp when created | | updatedAt | datetime | ✓ | ✓ | Timestamp of last update | | deletedAt | datetime | ✓ | ✓ | Soft delete timestamp (null if not deleted) | ## Example ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "number": "INV-2024-001", "status": "validated", "direction": "outgoing", "currency": "RON", "issueDate": "2024-02-15", "dueDate": "2024-03-15", "subtotal": 1000.00, "vatTotal": 190.00, "total": 1190.00, "clientName": "Acme Corporation SRL", "amountPaid": 500.00, "balance": 690.00, "client": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "type": "company", "name": "Acme Corporation SRL", "cui": "12345678", "vatCode": "RO12345678", "isVatPayer": true, "address": "Strada Exemplu, nr. 10", "city": "Bucharest", "email": "contact@acme.ro" }, "documentSeries": "INV", "discount": 0.00, "notes": "Payment by bank transfer within 30 days", "paymentTerms": "30 days", "invoiceTypeCode": "standard", "deliveryLocation": "Strada Exemplu, nr. 10, Bucharest", "exchangeRate": 1.0000, "anafUploadId": "4567890123", "anafUploadDate": "2024-02-15T10:30:00+02:00", "anafState": "validated", "pdfPath": "/storage/invoices/2024/02/INV-2024-001.pdf", "isIncoming": false, "isCreditNote": false, "lines": [ { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "position": 1, "description": "Web Development Services", "quantity": 40.0, "unitOfMeasure": "hour", "unitPrice": 25.00, "vatRate": 19.00, "vatAmount": 190.00, "lineTotal": 1190.00, "discount": 0.00 } ], "payments": [ { "id": "d4e5f6a7-b8c9-0123-def1-234567890123", "amount": 500.00, "currency": "RON", "paymentDate": "2024-02-20", "paymentMethod": "bank_transfer", "reference": "Transfer #12345" } ], "createdAt": "2024-02-15T09:00:00+02:00", "updatedAt": "2024-02-20T14:30:00+02:00", "deletedAt": null } ``` --- ## Invoice Line > Invoice line item object for products and services URL: https://docs.storno.ro/objects/invoice-line # Invoice Line The InvoiceLine object represents a single line item on an invoice, proforma, delivery note, or credit note. Lines reference products and include quantity, pricing, and VAT information. ## Attributes | Attribute | Type | Description | |-----------|------|-------------| | id | UUID | Unique identifier | | product | object | Product reference (id, name, code) - optional | | position | integer | Line position/order (1, 2, 3...) | | description | string | Line item description | | quantity | decimal | Quantity | | unitOfMeasure | string | Unit of measure (e.g., "buc", "ora", "kg") | | unitPrice | decimal | Price per unit (excluding VAT if vatIncluded is false) | | vatRate | decimal | VAT percentage rate (e.g., 19.00) | | vatCategoryCode | string | ANAF VAT category code (S, Z, E, etc.) | | vatAmount | decimal | Total VAT amount for this line | | lineTotal | decimal | Total line amount including VAT | | discount | decimal | Discount amount | | discountPercent | decimal | Discount percentage | | vatIncluded | boolean | Whether unitPrice includes VAT | | productCode | string | Product code/SKU | | ublExtensions | object | UBL extension fields (invoicePeriod, allowanceCharges, additionalItemProperties, originCountry) | | createdAt | datetime | Timestamp when created | | updatedAt | datetime | Timestamp of last update | ## Calculations The line calculations work as follows: ### When VAT is NOT included in price (vatIncluded = false) ``` subtotal = (unitPrice * quantity) - discount vatAmount = subtotal * (vatRate / 100) lineTotal = subtotal + vatAmount ``` ### When VAT is included in price (vatIncluded = true) ``` lineTotal = (unitPrice * quantity) - discount subtotal = lineTotal / (1 + vatRate / 100) vatAmount = lineTotal - subtotal ``` ## Example - Standard Line ```json { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "product": { "id": "d4e5f6a7-b8c9-0123-def1-234567890123", "name": "Web Development Services", "code": "SRV-WEBDEV" }, "position": 1, "description": "Custom web development - 40 hours", "quantity": 40.0, "unitOfMeasure": "ora", "unitPrice": 150.00, "vatRate": 19.00, "vatCategoryCode": "S", "vatAmount": 1140.00, "lineTotal": 7140.00, "discount": 0.00, "discountPercent": 0.00, "vatIncluded": false, "productCode": "SRV-WEBDEV", "createdAt": "2024-02-15T09:00:00+02:00", "updatedAt": "2024-02-15T09:00:00+02:00" } ``` ## Example - Line with Discount ```json { "id": "d4e5f6a7-b8c9-0123-def1-234567890123", "product": { "id": "e5f6a7b8-c9d0-1234-ef12-345678901234", "name": "Laptop Dell XPS 15", "code": "DELL-XPS15" }, "position": 1, "description": "Laptop Dell XPS 15 - 10% discount applied", "quantity": 5.0, "unitOfMeasure": "buc", "unitPrice": 5500.00, "vatRate": 19.00, "vatCategoryCode": "S", "vatAmount": 4655.00, "lineTotal": 29155.00, "discount": 2750.00, "discountPercent": 10.00, "vatIncluded": false, "productCode": "DELL-XPS15", "createdAt": "2024-02-15T10:00:00+02:00", "updatedAt": "2024-02-15T10:00:00+02:00" } ``` ## Example - Multiple Lines ```json [ { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "position": 1, "description": "Web Development Services", "quantity": 40.0, "unitOfMeasure": "ora", "unitPrice": 150.00, "vatRate": 19.00, "vatAmount": 1140.00, "lineTotal": 7140.00, "discount": 0.00, "productCode": "SRV-WEBDEV" }, { "id": "d4e5f6a7-b8c9-0123-def1-234567890123", "position": 2, "description": "Hosting Service - 1 year", "quantity": 12.0, "unitOfMeasure": "luna", "unitPrice": 50.00, "vatRate": 19.00, "vatAmount": 114.00, "lineTotal": 714.00, "discount": 0.00, "productCode": "HOST-YEAR" }, { "id": "e5f6a7b8-c9d0-1234-ef12-345678901234", "position": 3, "description": "SSL Certificate", "quantity": 1.0, "unitOfMeasure": "buc", "unitPrice": 100.00, "vatRate": 19.00, "vatAmount": 19.00, "lineTotal": 119.00, "discount": 0.00, "productCode": "SSL-CERT" } ] ``` ## Example - Credit Note Line (Negative) ```json { "id": "f6a7b8c9-d0e1-2345-f123-456789012345", "position": 1, "description": "Web Development Services (credit - overpayment)", "quantity": -10.0, "unitOfMeasure": "ora", "unitPrice": 150.00, "vatRate": 19.00, "vatCategoryCode": "S", "vatAmount": -285.00, "lineTotal": -1785.00, "discount": 0.00, "discountPercent": 0.00, "vatIncluded": false, "productCode": "SRV-WEBDEV", "createdAt": "2024-02-20T13:00:00+02:00", "updatedAt": "2024-02-20T13:00:00+02:00" } ``` ## Notes - **position**: Determines display order (1 is first line, 2 is second, etc.) - **product**: Optional reference to Product object (can be null for custom lines) - **description**: Free text description shown on invoice (can differ from product name) - **quantity**: Can be negative for credit notes - **unitPrice**: Price per unit, interpretation depends on `vatIncluded` - **vatIncluded**: - `false` - Price excludes VAT (most common for B2B) - `true` - Price includes VAT (common for B2C retail) - **discount**: Can be absolute amount or calculated from `discountPercent` - **productCode**: Stored for reference even if product is later deleted - All amounts are calculated and stored for performance (not computed on-the-fly) - Line items share the `DocumentLineFieldsTrait` with other document types --- ## Payment > Payment object for tracking invoice payments URL: https://docs.storno.ro/objects/payment # Payment The Payment object represents a payment received or made for an invoice. Multiple payments can be associated with a single invoice to track partial payments. ## Attributes | Attribute | Type | Description | |-----------|------|-------------| | id | UUID | Unique identifier | | amount | decimal | Payment amount | | currency | string | 3-letter currency code (e.g., "RON", "EUR") | | paymentDate | date | Date the payment was received/made (YYYY-MM-DD) | | paymentMethod | string | Payment method (bank_transfer, cash, card, etc.) | | reference | string | Payment reference/transaction ID | | notes | text | Additional notes about the payment | | isReconciled | boolean | Whether payment has been reconciled with bank statement | | paymentCreatedAt | datetime | Virtual field: timestamp when payment was recorded | | createdAt | datetime | Timestamp when created | | updatedAt | datetime | Timestamp of last update | ## Payment Methods Common payment methods include: - **bank_transfer** - Bank transfer (Ordin de plată) - **cash** - Cash payment (Numerar) - **card** - Card payment (Card) - **check** - Check (Cec) - **paypal** - PayPal - **stripe** - Stripe - **mobilpay** - MobilPay - **other** - Other method ## Example - Single Payment ```json { "id": "d4e5f6a7-b8c9-0123-def1-234567890123", "amount": 1190.00, "currency": "RON", "paymentDate": "2024-02-20", "paymentMethod": "bank_transfer", "reference": "OP-2024-1234", "notes": "Payment via Banca Transilvania", "isReconciled": true, "paymentCreatedAt": "2024-02-20T14:30:00+02:00", "createdAt": "2024-02-20T14:30:00+02:00", "updatedAt": "2024-02-21T09:00:00+02:00" } ``` ## Example - Partial Payments ```json [ { "id": "d4e5f6a7-b8c9-0123-def1-234567890123", "amount": 500.00, "currency": "RON", "paymentDate": "2024-02-20", "paymentMethod": "bank_transfer", "reference": "OP-2024-1234", "notes": "Partial payment 1 of 2", "isReconciled": true, "createdAt": "2024-02-20T14:30:00+02:00" }, { "id": "e5f6a7b8-c9d0-1234-ef12-345678901234", "amount": 690.00, "currency": "RON", "paymentDate": "2024-03-05", "paymentMethod": "bank_transfer", "reference": "OP-2024-2456", "notes": "Final payment", "isReconciled": false, "createdAt": "2024-03-05T10:15:00+02:00" } ] ``` ## Example - Cash Payment ```json { "id": "f6a7b8c9-d0e1-2345-f123-456789012345", "amount": 250.00, "currency": "RON", "paymentDate": "2024-02-18", "paymentMethod": "cash", "reference": "CASH-001", "notes": "Received in cash at office", "isReconciled": true, "createdAt": "2024-02-18T16:45:00+02:00", "updatedAt": "2024-02-18T16:45:00+02:00" } ``` ## Example - Card Payment ```json { "id": "a7b8c9d0-e1f2-3456-1234-567890123456", "amount": 595.00, "currency": "RON", "paymentDate": "2024-02-16", "paymentMethod": "stripe", "reference": "ch_3AbCdEfGhIjKlMnO", "notes": "Online payment via Stripe", "isReconciled": true, "createdAt": "2024-02-16T11:23:45+02:00", "updatedAt": "2024-02-16T11:23:45+02:00" } ``` ## Payment Reconciliation The `isReconciled` field tracks whether a payment has been verified against a bank statement: - **true**: Payment confirmed and matched with bank records - **false**: Payment recorded but not yet reconciled ## Invoice Balance Calculation The invoice balance is calculated as: ``` balance = invoice.total - SUM(payments.amount) ``` When `balance = 0`, the invoice status changes to `paid`. ## Notes - **currency**: Should match the invoice currency - **paymentDate**: Date when payment was actually received (can differ from creation date) - **reference**: Transaction ID, order number, or payment reference for tracking - **isReconciled**: Used for accounting purposes to track bank reconciliation - Multiple payments can be added to track installment payments - Payments cannot be deleted once reconciled (only marked as reconciled) - Overpayments (total payments > invoice total) are allowed and tracked - Payment records are preserved even if the invoice is soft-deleted --- ## PDF Template Config > PDF template configuration object for customizing document appearance URL: https://docs.storno.ro/objects/pdf-template-config # PDF Template Config The PDF Template Config object controls how PDFs are generated for a company's invoices, proforma invoices, credit notes, and delivery notes. Each company has a single configuration that applies to all document types. ## Object ```json { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "templateSlug": "classic", "primaryColor": "#2563eb", "fontFamily": "DejaVu Sans", "showLogo": true, "showBankInfo": true, "footerText": null, "customCss": null } ``` ## Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier (UUID) | | `templateSlug` | string | Active template design. One of: `classic`, `modern`, `minimal`, `bold` | | `primaryColor` | string\|null | Primary brand color in hex format (`#RRGGBB`). Falls back to template default if null. | | `fontFamily` | string\|null | CSS font family for the PDF. Default: `DejaVu Sans` | | `showLogo` | boolean | Whether to display the company logo. Default: `true` | | `showBankInfo` | boolean | Whether to display bank account information. Default: `true` | | `footerText` | string\|null | Custom footer text displayed at the bottom of documents | | `customCss` | string\|null | Custom CSS styles injected into the PDF template | ## Available Templates | Slug | Name | Default Color | Description | |------|------|---------------|-------------| | `classic` | Clasic | `#2563eb` | Traditional design with clean lines and professional colors | | `modern` | Modern | `#6366f1` | Modern design with rounded corners and colored header | | `minimal` | Minimal | `#374151` | Minimalist design with thin lines and compact layout | | `bold` | Bold | `#dc2626` | Bold design with color bar and large totals | ## Document Language PDFs support multi-language generation. The language is set per-document (not per-template) via the `language` field on invoices and proforma invoices. Supported languages: | Code | Language | |------|----------| | `ro` | Romanian (default) | | `en` | English | | `de` | German | | `fr` | French | ## Related Endpoints - [Get PDF Template Configuration](/api-reference/pdf-template-config/get) - [Update PDF Template Configuration](/api-reference/pdf-template-config/update) - [List Available Templates](/api-reference/pdf-template-config/list-templates) - [Preview PDF Template](/api-reference/pdf-template-config/preview) --- ## Product > Product or service object for invoice line items URL: https://docs.storno.ro/objects/product # Product The Product object represents goods or services that can be added to invoices, proformas, and other documents. Products can be physical goods or services, and can be synced from ANAF e-Factura system. ## Attributes | Attribute | Type | In List | In Detail | Description | |-----------|------|---------|-----------|-------------| | id | UUID | ✓ | ✓ | Unique identifier | | name | string | ✓ | ✓ | Product or service name | | code | string | ✓ | ✓ | Product code/SKU | | unitOfMeasure | string | ✓ | ✓ | Unit of measure (e.g., "buc", "kg", "ora", "m") | | defaultPrice | decimal | ✓ | ✓ | Default unit price | | currency | string | ✓ | ✓ | 3-letter currency code (e.g., "RON", "EUR") | | vatRate | decimal | ✓ | ✓ | VAT rate percentage (e.g., 19.00, 9.00, 5.00) | | isService | boolean | ✓ | ✓ | True if service, false if physical product | | isActive | boolean | ✓ | ✓ | Whether the product is active and available | | color | string \| null | ✓ | ✓ | Optional hex colour swatch (e.g. `"#1e40af"`) shown on the POS product grid. When null, mobile clients fall back to a deterministic palette derived from the product UUID. | | category | object \| null | ✓ | ✓ | Optional [ProductCategory](/objects/product-category) reference — `{ id, name, color, sortOrder }`. Used as fallback swatch and grid grouping on the POS. | | sgrAmount | string \| null | ✓ | ✓ | Romanian SGR (Sistem Garantie-Returnare) deposit per unit, e.g. `"0.50"` for plastic beverage bottles. Null when the product is not SGR-eligible. The deposit is VAT-exempt and appears as a separate auto-managed line on POS receipts. | | description | text | ✗ | ✓ | Detailed description | | vatCategoryCode | string | ✗ | ✓ | VAT category code for ANAF (e.g., "S", "Z", "E") | | source | string | ✗ | ✓ | Source: manual, anaf, import | | lastSyncedAt | datetime | ✗ | ✓ | Last sync timestamp from ANAF | | createdAt | datetime | ✓ | ✓ | Timestamp when created | | updatedAt | datetime | ✓ | ✓ | Timestamp of last update | | deletedAt | datetime | ✓ | ✓ | Soft delete timestamp (null if not deleted) | ## Example - Physical Product ```json { "id": "d4e5f6a7-b8c9-0123-def1-234567890123", "name": "Laptop Dell XPS 15", "code": "DELL-XPS15-2024", "unitOfMeasure": "buc", "defaultPrice": 5500.00, "currency": "RON", "vatRate": 19.00, "isService": false, "isActive": true, "description": "Laptop Dell XPS 15, Intel i7, 16GB RAM, 512GB SSD", "vatCategoryCode": "S", "source": "manual", "lastSyncedAt": null, "createdAt": "2024-01-10T09:00:00+02:00", "updatedAt": "2024-02-05T11:30:00+02:00", "deletedAt": null } ``` ## Example - Service ```json { "id": "e5f6a7b8-c9d0-1234-ef12-345678901234", "name": "Web Development", "code": "SRV-WEBDEV", "unitOfMeasure": "ora", "defaultPrice": 150.00, "currency": "RON", "vatRate": 19.00, "isService": true, "isActive": true, "description": "Custom web development services - hourly rate", "vatCategoryCode": "S", "source": "manual", "lastSyncedAt": null, "createdAt": "2024-01-05T10:00:00+02:00", "updatedAt": "2024-01-05T10:00:00+02:00", "deletedAt": null } ``` ## Common Units of Measure (Romania) - **buc** - bucată (piece) - **kg** - kilogram - **g** - gram - **l** - liter - **m** - meter - **m2** - square meter - **m3** - cubic meter - **ora** - hour (oră) - **zi** - day (zi) - **luna** - month (lună) - **set** - set - **pachet** - package (pachet) ## VAT Category Codes - **S** - Standard rate (19%) - **Z** - Zero rate (0%) - **E** - Exempt - **AE** - Reverse charge - **K** - Intra-community supply - **G** - Export outside EU - **O** - Outside scope - **L** - Canary Islands levy - **M** - Ceuta/Melilla levy ## Notes - **isService**: Affects how the product is treated in invoices (services vs goods) - **isActive**: Inactive products don't appear in product selection dropdowns - **defaultPrice**: Used as starting price when adding to invoice lines - **vatRate**: Default VAT rate, can be overridden on individual invoice lines - **source**: `manual` (user-created), `anaf` (synced from e-Factura), `import` (bulk import) - Products synced from ANAF have `lastSyncedAt` timestamp - Soft-deleted products have `deletedAt` set but remain in database ## POS fields `color`, `category`, and `sgrAmount` are POS-specific and have no effect on regular invoice flows. - **color**: 6-digit hex swatch (with `#` prefix). Drives the tile colour on the mobile POS product grid. - **category**: Optional FK to [ProductCategory](/objects/product-category). Drives the category quick-filter chips above the POS grid; when `color` is null, the category colour is used as the swatch fallback. - **sgrAmount**: Romanian SGR deposit (Sistem Garantie-Returnare). Decimal stored as string (e.g. `"0.50"`). When non-null, the POS auto-appends a separate VAT-exempt line for the deposit on each sale. Cancelling or refunding the parent line cancels/refunds the deposit line in lockstep. --- ## Product Category > Product category for grouping and filtering on the POS product grid URL: https://docs.storno.ro/objects/product-category # Product Category The ProductCategory object groups [Products](/objects/product) into a small, ordered set of buckets used by the mobile POS. Categories drive the chip strip above the product grid (tap to filter) and supply a fallback colour swatch for product tiles when `Product.color` is null. ## Attributes | Attribute | Type | Description | |-----------|------|-------------| | id | UUID | Unique identifier | | name | string | Display name (1–100 chars) | | color | string \| null | Optional hex swatch (`#RRGGBB`) used for the chip background and as a fallback Product tile colour | | sortOrder | integer | Sort key (smaller = earlier). Defaults to 0 | | createdAt | datetime | Timestamp when created | | updatedAt | datetime | Timestamp of last update | ## Example ```json { "id": "0a1b2c3d-4e5f-6789-abcd-ef0123456789", "name": "Cafele", "color": "#7c3aed", "sortOrder": 0, "createdAt": "2026-04-20T09:00:00+03:00", "updatedAt": "2026-04-20T09:00:00+03:00" } ``` ## Notes - Categories are scoped to a single Company (via the `X-Company` header). - `Product.category` is an optional FK with `ON DELETE SET NULL` — deleting a category does not delete its products; the products simply become uncategorised. - The mobile POS sorts the chip strip by `sortOrder` ascending, then by `name` ascending as a tie-breaker. - When a Product tile's `color` is null, the POS falls back to the category colour; when both are null, it uses a deterministic palette derived from the product UUID. ## Related Endpoints - [List product categories](/api-reference/product-categories/list) - [Create product category](/api-reference/product-categories/create) - [Update product category](/api-reference/product-categories/update) - [Delete product category](/api-reference/product-categories/delete) --- ## Proforma Invoice > Proforma invoice object for quotations and advance billing URL: https://docs.storno.ro/objects/proforma-invoice # Proforma Invoice The ProformaInvoice object represents proforma invoices (quotations) that can be sent to clients before issuing a final invoice. Proforma invoices can be accepted, rejected, or converted into regular invoices. ## Attributes | Attribute | Type | In List | In Detail | Description | |-----------|------|---------|-----------|-------------| | id | UUID | ✓ | ✓ | Unique identifier | | number | string | ✓ | ✓ | Proforma number (e.g., "PRO-2024-001") | | status | ProformaStatus | ✓ | ✓ | Status: draft, sent, accepted, rejected, converted, cancelled | | currency | string | ✓ | ✓ | 3-letter currency code (e.g., "RON", "EUR") | | language | string | ✗ | ✓ | Document language for PDF generation: `ro`, `en`, `de`, `fr` (default: `ro`) | | issueDate | date | ✓ | ✓ | Date the proforma was issued (YYYY-MM-DD) | | dueDate | date | ✓ | ✓ | Payment due date (YYYY-MM-DD) | | subtotal | decimal | ✓ | ✓ | Subtotal before VAT | | vatTotal | decimal | ✓ | ✓ | Total VAT amount | | total | decimal | ✓ | ✓ | Total amount including VAT | | clientName | string | ✓ | ✓ | Virtual field: name of the client | | client | Client | ✗ | ✓ | Full Client object | | documentSeries | string | ✗ | ✓ | Document series prefix | | validUntil | date | ✗ | ✓ | Date until which the proforma is valid | | discount | decimal | ✗ | ✓ | Total discount amount | | notes | text | ✗ | ✓ | Public notes visible to client | | paymentTerms | text | ✗ | ✓ | Payment terms description | | invoiceTypeCode | InvoiceTypeCode | ✗ | ✓ | Type code (standard, reverse_charge, etc.) | | deliveryLocation | string | ✗ | ✓ | Delivery address | | projectReference | string | ✗ | ✓ | Project or reference number | | orderNumber | string | ✗ | ✓ | Purchase order number | | contractNumber | string | ✗ | ✓ | Contract reference number | | issuerName | string | ✗ | ✓ | Name of the person issuing the proforma | | issuerId | string | ✗ | ✓ | ID/CNP of the issuer | | mentions | text | ✗ | ✓ | Additional mentions on proforma | | internalNote | text | ✗ | ✓ | Internal notes (not visible to client) | | salesAgent | string | ✗ | ✓ | Sales agent name | | exchangeRate | decimal | ✗ | ✓ | Exchange rate used (for foreign currencies) | | convertedInvoice | object | ✗ | ✓ | Reference to converted invoice (if status is converted) | | sentAt | datetime | ✗ | ✓ | Timestamp when sent to client | | acceptedAt | datetime | ✗ | ✓ | Timestamp when accepted by client | | rejectedAt | datetime | ✗ | ✓ | Timestamp when rejected by client | | cancelledAt | datetime | ✗ | ✓ | Timestamp when cancelled | | lines | array | ✗ | ✓ | Array of ProformaInvoiceLine objects | | createdAt | datetime | ✓ | ✓ | Timestamp when created | | updatedAt | datetime | ✓ | ✓ | Timestamp of last update | | deletedAt | datetime | ✓ | ✓ | Soft delete timestamp (null if not deleted) | ## Example ```json { "id": "e5f6a7b8-c9d0-1234-ef12-345678901234", "number": "PRO-2024-001", "status": "sent", "currency": "RON", "issueDate": "2024-02-15", "dueDate": "2024-03-15", "subtotal": 2500.00, "vatTotal": 475.00, "total": 2975.00, "clientName": "Tech Solutions SRL", "client": { "id": "f6a7b8c9-d0e1-2345-f123-456789012345", "type": "company", "name": "Tech Solutions SRL", "cui": "23456789", "vatCode": "RO23456789", "isVatPayer": true, "address": "Bulevardul Unirii, nr. 25", "city": "Cluj-Napoca", "email": "office@techsolutions.ro" }, "documentSeries": "PRO", "validUntil": "2024-03-31", "discount": 0.00, "notes": "Valid for 45 days. Payment within 30 days after acceptance.", "paymentTerms": "30 days from acceptance", "invoiceTypeCode": "standard", "deliveryLocation": "Bulevardul Unirii, nr. 25, Cluj-Napoca", "projectReference": "PROJ-2024-005", "exchangeRate": 1.0000, "convertedInvoice": null, "sentAt": "2024-02-15T11:00:00+02:00", "acceptedAt": null, "rejectedAt": null, "cancelledAt": null, "lines": [ { "id": "a7b8c9d0-e1f2-3456-1234-567890123456", "position": 1, "description": "Custom Software Development", "quantity": 100.0, "unitOfMeasure": "hour", "unitPrice": 25.00, "vatRate": 19.00, "vatAmount": 475.00, "lineTotal": 2975.00, "discount": 0.00 } ], "createdAt": "2024-02-15T10:00:00+02:00", "updatedAt": "2024-02-15T11:00:00+02:00", "deletedAt": null } ``` --- ## Receipt > Receipt (Bon Fiscal) object for cash register fiscal documentation URL: https://docs.storno.ro/objects/receipt # Receipt The Receipt object represents a fiscal receipt (bon fiscal) issued from a cash register. Receipts document point-of-sale transactions and can optionally be converted into invoices when the customer requires a formal tax document. ## Attributes | Attribute | Type | In List | In Detail | Description | |-----------|------|---------|-----------|-------------| | id | UUID | ✓ | ✓ | Unique identifier | | number | string | ✓ | ✓ | Receipt number (e.g., "BON-2024-001") | | status | ReceiptStatus | ✓ | ✓ | Status: draft, issued, invoiced, cancelled, refunded, partially_refunded | | currency | string | ✓ | ✓ | 3-letter currency code (e.g., "RON", "EUR") | | issueDate | date | ✓ | ✓ | Date the receipt was issued (YYYY-MM-DD) | | subtotal | decimal | ✓ | ✓ | Subtotal before VAT | | vatTotal | decimal | ✓ | ✓ | Total VAT amount | | total | decimal | ✓ | ✓ | Total amount including VAT | | paymentMethod | string | ✓ | ✓ | Payment method: cash, card, mixed, other | | cashPayment | decimal | ✓ | ✓ | Amount paid in cash | | cardPayment | decimal | ✓ | ✓ | Amount paid by card | | otherPayment | decimal | ✓ | ✓ | Amount paid by other method | | cashRegisterName | string | ✓ | ✓ | Name or identifier of the cash register | | fiscalNumber | string | ✓ | ✓ | Fiscal serial number of the cash register | | customerName | string | ✓ | ✓ | Customer name (optional, for B2B receipts) | | customerCif | string | ✓ | ✓ | Customer CIF/CUI (optional, for B2B receipts) | | clientName | string | ✓ | ✓ | Virtual field: name of the linked client (if any) | | client | Client | ✗ | ✓ | Full Client object (if linked) | | documentSeriesId | UUID | ✗ | ✓ | UUID of the assigned document series. Auto-assigned from the default `receipt` series if not specified at creation | | documentSeries | string | ✗ | ✓ | Document series prefix | | notes | text | ✗ | ✓ | Notes about the receipt | | internalNote | text | ✗ | ✓ | Internal notes (not visible to customer) | | convertedInvoice | object | ✗ | ✓ | Reference to converted invoice (if status is invoiced) | | refundOf | object \| null | ✓ | ✓ | If this receipt is a refund, slim `{id, number}` reference to the parent receipt being refunded. Null on regular receipts. | | refundedBy | array | ✗ | ✓ | Array of slim `{id, number}` references to active refund receipts issued against this receipt. Cancelled refunds are excluded. Empty array when nothing has been refunded. | | idempotencyKey | string \| null | ✗ | ✓ | Unique idempotency key (max 255 chars). Set via the `Idempotency-Key` HTTP header (preferred) or the `idempotencyKey` body field on `POST /receipts`. Repeat submissions with the same key return the originally-created receipt instead of duplicating. | | issuedAt | datetime | ✗ | ✓ | Timestamp when issued | | cancelledAt | datetime | ✗ | ✓ | Timestamp when cancelled | | lines | array | ✗ | ✓ | Array of ReceiptLine objects | | createdAt | datetime | ✓ | ✓ | Timestamp when created | | updatedAt | datetime | ✓ | ✓ | Timestamp of last update | | deletedAt | datetime | ✓ | ✓ | Soft delete timestamp (null if not deleted) | ## Example ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "number": "BON-2024-001", "status": "issued", "currency": "RON", "issueDate": "2024-02-18", "subtotal": 210.92, "vatTotal": 40.08, "total": 251.00, "paymentMethod": "mixed", "cashPayment": 100.00, "cardPayment": 151.00, "otherPayment": 0.00, "cashRegisterName": "Casa 1 - Front Desk", "fiscalNumber": "AAAA123456", "customerName": "Acme SRL", "customerCif": "RO12345678", "clientName": "Acme SRL", "client": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "type": "company", "name": "Acme SRL", "cui": "12345678", "vatCode": "RO12345678", "isVatPayer": true, "address": "Strada Principala, nr. 10", "city": "Cluj-Napoca", "email": "office@acme.ro" }, "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "documentSeries": "BON", "notes": "Thank you for your purchase!", "internalNote": "Loyalty card customer", "convertedInvoice": null, "issuedAt": "2024-02-18T10:15:00+02:00", "cancelledAt": null, "lines": [ { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "position": 1, "description": "Coffee - Espresso", "quantity": 2.0, "unitOfMeasure": "pcs", "unitPrice": 12.61, "vatRate": 9.00, "vatAmount": 2.27, "lineTotal": 27.49, "discount": 0.00, "productCode": "COF-ESP" }, { "id": "d4e5f6a7-b8c9-0123-defa-234567890123", "position": 2, "description": "Sandwich - Club", "quantity": 3.0, "unitOfMeasure": "pcs", "unitPrice": 29.41, "vatRate": 9.00, "vatAmount": 7.94, "lineTotal": 96.17, "discount": 0.00, "productCode": "SAN-CLB" }, { "id": "e5f6a7b8-c9d0-1234-efab-345678901234", "position": 3, "description": "Mineral Water 0.5L", "quantity": 5.0, "unitOfMeasure": "pcs", "unitPrice": 5.04, "vatRate": 9.00, "vatAmount": 2.27, "lineTotal": 27.47, "discount": 0.00, "productCode": "WAT-MIN-05" }, { "id": "f6a7b8c9-d0e1-2345-fabc-456789012345", "position": 4, "description": "Notebook A5", "quantity": 2.0, "unitOfMeasure": "pcs", "unitPrice": 41.18, "vatRate": 19.00, "vatAmount": 15.65, "lineTotal": 97.01, "discount": 0.00, "productCode": "NOTE-A5" } ], "createdAt": "2024-02-18T10:14:00+02:00", "updatedAt": "2024-02-18T10:15:00+02:00", "deletedAt": null } ``` ## Workflow 1. **Draft**: Receipt is prepared with line items before printing 2. **Issued**: Receipt is printed and handed to the customer 3. **Invoiced**: Customer requested a formal invoice; receipt was converted to an invoice 4. **Cancelled**: Receipt was voided (requires a cancellation receipt to be issued on the cash register) 5. **Partially refunded**: One or more refund receipts have been issued against this receipt, but at least one line still has remaining unrefunded quantity 6. **Refunded**: All line quantities have been refunded; no further refunds can be issued ## Refunds A refund receipt is a counter-receipt that mirrors the parent's lines as negative quantities and inverts the payment amounts. Issue one with `POST /receipts/{uuid}/refund`. The body accepts an optional `lineSelections` array of `{ position, quantity }` for partial refunds; omit it to refund the whole receipt. - Multiple partial refunds against the same parent are allowed until each line's quantity pool is exhausted. - Cancelling a refund (via `POST /receipts/{uuid}/cancel`) releases its quantities back to the pool. - The refund inherits `internalNote`, `cashRegisterName`, `fiscalNumber`, and customer fiscal data from the parent. - Refund PDFs render with a `BON DE RAMBURSARE` header (translated to `REFUND RECEIPT` / etc. per locale) instead of `BON FISCAL`. ## Idempotency To make POS retries safe across flaky networks, send an `Idempotency-Key` header on `POST /receipts`. The first request with a given key creates the receipt; subsequent requests with the same key return the original receipt without creating a duplicate. The same key may also be supplied as a body field `idempotencyKey`, but the HTTP header takes precedence when both are present. ## Payment Method Breakdown The receipt tracks how the customer paid: | paymentMethod | cashPayment | cardPayment | otherPayment | Description | |---------------|-------------|-------------|--------------|-------------| | `cash` | = total | 0 | 0 | Paid entirely in cash | | `card` | 0 | = total | 0 | Paid entirely by card | | `other` | 0 | 0 | = total | Voucher, meal ticket, crypto, etc. | | `mixed` | partial | partial | partial | Combination of payment types | The sum of `cashPayment + cardPayment + otherPayment` must always equal `total`. ## Notes - Receipts are issued from a physical or virtual fiscal cash register registered with ANAF - `fiscalNumber` is the unique serial number assigned to the cash register by the manufacturer - `cashRegisterName` is a user-defined label to identify which register issued the receipt - For B2B transactions where the business customer needs a formal invoice, use the convert endpoint - `customerName` and `customerCif` can be populated for B2B receipts even without linking to a Client object - Cancelled receipts must have a corresponding cancellation receipt printed on the fiscal device - PDFs are generated using the company's [PDF template configuration](/objects/pdf-template-config) --- ## Recurring Invoice > Recurring invoice template for automated invoice generation URL: https://docs.storno.ro/objects/recurring-invoice # Recurring Invoice The RecurringInvoice object represents templates for automatically generating invoices on a recurring schedule (monthly, quarterly, yearly). The system automatically creates invoices based on the frequency settings and optionally sends them via email. ## Attributes | Attribute | Type | In List | In Detail | Description | |-----------|------|---------|-----------|-------------| | id | UUID | ✓ | ✓ | Unique identifier | | reference | string | ✓ | ✓ | Reference name for this recurring invoice | | isActive | boolean | ✓ | ✓ | Whether automatic generation is active | | documentType | DocumentType | ✓ | ✓ | Type: invoice or credit_note | | currency | string | ✓ | ✓ | 3-letter currency code (e.g., "RON", "EUR") | | frequency | string | ✓ | ✓ | Frequency: monthly, quarterly, yearly | | frequencyDay | integer | ✓ | ✓ | Day of month to generate invoice (1-31) | | nextIssuanceDate | date | ✓ | ✓ | Next scheduled generation date (YYYY-MM-DD) | | lastIssuedAt | datetime | ✓ | ✓ | When the last invoice was generated | | lastInvoiceNumber | string | ✓ | ✓ | Number of the last generated invoice | | clientName | string | ✓ | ✓ | Virtual field: name of the client | | subtotal | decimal | ✓ | ✓ | Template subtotal before VAT | | vatTotal | decimal | ✓ | ✓ | Template total VAT amount | | total | decimal | ✓ | ✓ | Template total amount including VAT | | client | Client | ✗ | ✓ | Full Client object | | documentSeries | string | ✗ | ✓ | Document series to use for generated invoices | | invoiceTypeCode | InvoiceTypeCode | ✗ | ✓ | Type code for generated invoices | | autoEmailEnabled | boolean | ✗ | ✓ | Automatically email the generated invoice | | autoEmailTime | time | ✗ | ✓ | Time of day to send email (HH:MM) | | autoEmailDayOffset | integer | ✗ | ✓ | Days after generation to send email | | penaltyEnabled | boolean | ✗ | ✓ | Enable late payment penalties on generated invoices | | penaltyPercentPerDay | decimal | ✗ | ✓ | Daily penalty percentage | | penaltyGraceDays | integer | ✗ | ✓ | Grace period before penalties apply | | dueDateType | string | ✗ | ✓ | How to calculate due date: days_after, fixed_day | | dueDateDays | integer | ✗ | ✓ | Days after issue date (if dueDateType is days_after) | | dueDateFixedDay | integer | ✗ | ✓ | Fixed day of month (if dueDateType is fixed_day) | | frequencyMonth | integer | ✗ | ✓ | Month for yearly frequency (1-12) | | stopDate | date | ✗ | ✓ | Date to stop generating invoices (null = indefinite) | | notes | text | ✗ | ✓ | Notes to include on generated invoices | | paymentTerms | text | ✗ | ✓ | Payment terms for generated invoices | | lines | array | ✗ | ✓ | Array of RecurringInvoiceLine objects | | createdAt | datetime | ✓ | ✓ | Timestamp when created | | updatedAt | datetime | ✓ | ✓ | Timestamp of last update | | deletedAt | datetime | ✓ | ✓ | Soft delete timestamp (null if not deleted) | ## Recurring Invoice Line RecurringInvoiceLine extends the standard line with additional fields for dynamic pricing: | Attribute | Type | Description | |-----------|------|-------------| | referenceCurrency | string | Currency for reference price tracking | | markupPercent | decimal | Markup percentage to apply | | priceRule | string | Dynamic pricing rule (e.g., "latest_product_price", "apply_exchange_rate") | All standard line fields (description, quantity, unitOfMeasure, unitPrice, vatRate, etc.) are also available. ## Example ```json { "id": "a3b4c5d6-e7f8-9012-7890-123456789012", "reference": "Monthly Hosting - Acme Corp", "isActive": true, "documentType": "invoice", "currency": "RON", "frequency": "monthly", "frequencyDay": 1, "nextIssuanceDate": "2024-03-01", "lastIssuedAt": "2024-02-01T08:00:00+02:00", "lastInvoiceNumber": "INV-2024-045", "clientName": "Acme Corporation SRL", "subtotal": 500.00, "vatTotal": 95.00, "total": 595.00, "client": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "type": "company", "name": "Acme Corporation SRL", "cui": "12345678", "vatCode": "RO12345678", "isVatPayer": true, "email": "billing@acme.ro" }, "documentSeries": "INV", "invoiceTypeCode": "standard", "autoEmailEnabled": true, "autoEmailTime": "09:00", "autoEmailDayOffset": 0, "penaltyEnabled": true, "penaltyPercentPerDay": 0.05, "penaltyGraceDays": 5, "dueDateType": "days_after", "dueDateDays": 30, "dueDateFixedDay": null, "frequencyMonth": null, "stopDate": null, "notes": "Monthly hosting service subscription", "paymentTerms": "Payment due within 30 days", "lines": [ { "id": "b4c5d6e7-f8a9-0123-8901-234567890123", "position": 1, "description": "Web Hosting Service - Premium Plan", "quantity": 1.0, "unitOfMeasure": "month", "unitPrice": 500.00, "vatRate": 19.00, "vatAmount": 95.00, "lineTotal": 595.00, "productCode": "HOST-PREMIUM", "referenceCurrency": "EUR", "markupPercent": 0.00, "priceRule": "latest_product_price" } ], "createdAt": "2024-01-01T10:00:00+02:00", "updatedAt": "2024-02-01T08:00:00+02:00", "deletedAt": null } ``` ## Frequency Options - **monthly**: Generated on `frequencyDay` of each month - **quarterly**: Generated every 3 months on `frequencyDay` - **yearly**: Generated once per year on `frequencyDay` of month `frequencyMonth` ## Dynamic Pricing Rules - **latest_product_price**: Use the current product price at generation time - **apply_exchange_rate**: Convert from `referenceCurrency` using latest exchange rate - **fixed**: Use the template price (default) ## Notes - The system automatically generates invoices based on `nextIssuanceDate` - After generation, `nextIssuanceDate` is advanced based on frequency - If `autoEmailEnabled`, the invoice is sent to client's email - Recurring invoices can be paused by setting `isActive: false` - They automatically stop generating after `stopDate` (if set) --- ## Supplier > Supplier object for incoming invoices URL: https://docs.storno.ro/objects/supplier # Supplier The Supplier object represents companies that issue incoming invoices to your company. Suppliers are typically synced from e-invoice provider systems when incoming invoices are downloaded. ## Attributes | Attribute | Type | In List | In Detail | Description | |-----------|------|---------|-----------|-------------| | id | UUID | ✓ | ✓ | Unique identifier | | name | string | ✓ | ✓ | Supplier company name | | cif | string | ✓ | ✓ | CUI/CIF (Cod Unic de Identificare) | | vatCode | string | ✓ | ✓ | Full VAT code with country prefix (e.g., "RO12345678") | | isVatPayer | boolean | ✓ | ✓ | Whether the supplier is registered for VAT | | address | string | ✓ | ✓ | Street address | | city | string | ✓ | ✓ | City name | | email | string | ✓ | ✓ | Email address | | registrationNumber | string | ✗ | ✓ | Company registration number (număr de înregistrare) | | county | string | ✗ | ✓ | County/state | | country | string | ✗ | ✓ | Country code (e.g., "RO", "DE") | | postalCode | string | ✗ | ✓ | Postal/ZIP code | | phone | string | ✗ | ✓ | Phone number | | bankName | string | ✗ | ✓ | Bank name | | bankAccount | string | ✗ | ✓ | Bank account number (IBAN) | | notes | text | ✗ | ✓ | Internal notes about the supplier | | source | string | ✗ | ✓ | Source: manual, anaf, import | | lastSyncedAt | datetime | ✗ | ✓ | Last sync timestamp from e-invoice provider | | createdAt | datetime | ✓ | ✓ | Timestamp when created | | updatedAt | datetime | ✓ | ✓ | Timestamp of last update | | deletedAt | datetime | ✓ | ✓ | Soft delete timestamp (null if not deleted) | ## Example ```json { "id": "f6a7b8c9-d0e1-2345-f123-456789012345", "name": "Tech Supplies SRL", "cif": "98765432", "vatCode": "RO98765432", "isVatPayer": true, "address": "Calea Victoriei, nr. 123", "city": "Bucharest", "email": "office@techsupplies.ro", "registrationNumber": "J40/9876/2018", "county": "Bucuresti", "country": "RO", "postalCode": "010101", "phone": "+40 21 987 6543", "bankName": "BCR", "bankAccount": "RO49RNCB0082045678910123", "notes": "Preferred supplier for IT equipment", "source": "anaf", "lastSyncedAt": "2024-02-15T08:30:00+02:00", "createdAt": "2024-01-10T12:00:00+02:00", "updatedAt": "2024-02-15T08:30:00+02:00", "deletedAt": null } ``` ## Notes - Suppliers are primarily used for incoming invoices - Most suppliers are automatically created when downloading invoices from an e-invoice provider - **source**: - `anaf` - Automatically created from e-invoice provider (ANAF in Romania) - `manual` - Manually created by user - `import` - Bulk imported - **lastSyncedAt**: Updated when supplier data is refreshed from the e-invoice provider - Supplier data is extracted from the invoice XML when syncing from the e-invoice provider - Soft-deleted suppliers have `deletedAt` set but remain in database --- ## User > User object representing an authenticated user URL: https://docs.storno.ro/objects/user # User The User object represents an authenticated user in the system. User data is returned by the `/api/v1/me` endpoint and includes profile information, organization membership, and permissions. ## Attributes | Attribute | Type | Description | |-----------|------|-------------| | id | UUID | Unique identifier | | email | string | User's email address | | firstName | string | First name | | lastName | string | Last name | | phone | string | Phone number | | locale | string | Preferred locale (e.g., "ro", "en") | | timezone | string | User's timezone (e.g., "Europe/Bucharest") | | roles | array | Global roles (typically empty for normal users) | | credits | integer | Available credits for services | | active | boolean | Whether the user account is active | | lastConnectedAt | datetime | Last login timestamp | | emailVerified | boolean | Whether email is verified | | googleId | string/null | Google OAuth ID (if connected) | | preferences | object | User preferences and settings | | organization | object | User's organization data | | memberships | array | Array of OrganizationMembership objects | | createdAt | datetime | Timestamp when created | | updatedAt | datetime | Timestamp of last update | ## Organization Membership Each membership includes: | Attribute | Type | Description | |-----------|------|-------------| | id | UUID | Membership ID | | role | OrganizationRole | Role: owner, admin, accountant, employee | | permissions | array | Array of permission strings | | allowedCompanies | array | Array of company IDs the user can access | | organization | object | Organization details (id, name) | ## Example ```json { "id": "b8c9d0e1-f2a3-4567-2345-678901234567", "email": "john.doe@example.ro", "firstName": "John", "lastName": "Doe", "phone": "+40 744 123 456", "locale": "ro", "timezone": "Europe/Bucharest", "roles": [], "credits": 1000, "active": true, "lastConnectedAt": "2024-02-16T09:30:00+02:00", "emailVerified": true, "googleId": "1234567890abcdef", "preferences": { "theme": "light", "notifications": { "email": true, "push": false }, "dashboard": { "defaultView": "invoices", "itemsPerPage": 25 } }, "organization": { "id": "c9d0e1f2-a3b4-5678-3456-789012345678", "name": "Example Tech SRL", "slug": "example-tech", "createdAt": "2024-01-01T10:00:00+02:00" }, "memberships": [ { "id": "d0e1f2a3-b4c5-6789-4567-890123456789", "role": "admin", "permissions": [ "invoices.create", "invoices.edit", "invoices.delete", "invoices.view", "clients.manage", "products.manage", "settings.view" ], "allowedCompanies": [ "a7b8c9d0-e1f2-3456-1234-567890123456", "e1f2a3b4-c5d6-7890-5678-901234567890" ], "organization": { "id": "c9d0e1f2-a3b4-5678-3456-789012345678", "name": "Example Tech SRL" } } ], "createdAt": "2024-01-01T11:00:00+02:00", "updatedAt": "2024-02-16T09:30:00+02:00" } ``` ## Organization Roles - **owner**: Full access to all features and settings - **admin**: Administrative access, can manage users and settings - **accountant**: Can manage financial documents and reports - **employee**: Limited access based on assigned permissions ## Common Permissions - **invoices.create** - Create new invoices - **invoices.edit** - Edit existing invoices - **invoices.delete** - Delete invoices - **invoices.view** - View invoices - **clients.manage** - Manage clients - **products.manage** - Manage products - **settings.view** - View company settings - **settings.edit** - Edit company settings - **reports.view** - Access reports - **users.manage** - Manage organization users ## Notes - The `/api/v1/me` endpoint returns flat user JSON (NOT `{user: {...}}`) - Multi-company access is controlled through `allowedCompanies` in membership - Current company context is selected via `X-Company` header in API requests - **googleId**: Present if user signed in with Google OAuth - **preferences**: Stored as JSON, structure is flexible - Users can belong to multiple organizations (one membership per organization) --- ## VAT Rate > VAT rate configuration object URL: https://docs.storno.ro/objects/vat-rate # VAT Rate The VatRate object represents a configured VAT rate that can be applied to invoice line items. Romanian companies typically use rates of 19%, 9%, 5%, and 0%. ## Attributes | Attribute | Type | Description | |-----------|------|-------------| | id | UUID | Unique identifier | | rate | decimal | VAT percentage rate (e.g., 19.00, 9.00, 5.00, 0.00) | | label | string | Display label (e.g., "Standard 19%", "Reduced 9%") | | categoryCode | string | ANAF category code (S, Z, E, AE, etc.) | | isDefault | boolean | Whether this is the default VAT rate | | isActive | boolean | Whether this rate is active and available | | position | integer | Sort order for display | | createdAt | datetime | Timestamp when created | | updatedAt | datetime | Timestamp of last update | ## Example ```json { "id": "a3b4c5d6-e7f8-9012-7890-123456789012", "rate": 19.00, "label": "Standard 19%", "categoryCode": "S", "isDefault": true, "isActive": true, "position": 1, "createdAt": "2024-01-01T10:00:00+02:00", "updatedAt": "2024-01-01T10:00:00+02:00" } ``` ## Common Romanian VAT Rates ```json [ { "id": "a3b4c5d6-e7f8-9012-7890-123456789012", "rate": 19.00, "label": "Standard 19%", "categoryCode": "S", "isDefault": true, "isActive": true, "position": 1 }, { "id": "b4c5d6e7-f8a9-0123-8901-234567890123", "rate": 9.00, "label": "Reduced 9%", "categoryCode": "S", "isDefault": false, "isActive": true, "position": 2 }, { "id": "c5d6e7f8-a9b0-1234-9012-345678901234", "rate": 5.00, "label": "Super-reduced 5%", "categoryCode": "S", "isDefault": false, "isActive": true, "position": 3 }, { "id": "d6e7f8a9-b0c1-2345-0123-456789012345", "rate": 0.00, "label": "Zero rate 0%", "categoryCode": "Z", "isDefault": false, "isActive": true, "position": 4 }, { "id": "e7f8a9b0-c1d2-3456-1234-567890123456", "rate": 0.00, "label": "Exempt", "categoryCode": "E", "isDefault": false, "isActive": true, "position": 5 } ] ``` ## VAT Category Codes (ANAF) | Code | Description | |------|-------------| | S | Standard rate (19%, 9%, 5%) | | Z | Zero rate (0%) | | E | Exempt from VAT | | AE | Reverse charge (Taxare inversă) | | K | Intra-community supply | | G | Export outside EU | | O | Outside scope of VAT | | L | Canary Islands general indirect tax | | M | Tax for production, services and importation in Ceuta and Melilla | ## Romanian VAT Rate History - **19%** - Standard rate (most goods and services) - **9%** - Reduced rate (food, water supply, restaurants, hotels, cultural services) - **5%** - Super-reduced rate (social housing, certain books, newspapers) - **0%** - Zero rate (intra-community deliveries, exports) ## Notes - Only one VAT rate should be marked as `isDefault: true` - The default rate is pre-selected when adding new products or invoice lines - **rate**: Stored as decimal percentage (19.00, not 0.19) - **categoryCode**: Must match ANAF e-Factura requirements - **position**: Controls the display order in dropdowns (lower = first) - Inactive rates (`isActive: false`) are hidden from selection but remain in system - Historical invoices maintain their VAT rate even if the rate is later deactivated --- ## Accounting export settings > Read and update the company's stored SAGA / WinMentor / Ciel export defaults URL: https://docs.storno.ro/api-reference/accounting-export/settings # Accounting export settings Per-company defaults consumed by [`POST /api/v1/accounting-export/zip`](/api-reference/accounting-export/zip). The stored values are used unless an export request passes explicit overrides via `options.accounts`. ## Get settings ``` GET /api/v1/accounting-export/settings ``` ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ### Response ```json { "saga": { "accountCash": "5311", "accountBank": "5121", "accountCard": "5125.2", "accountClients": "4111", "accountSuppliers": "4011", "currencyAccounts": { "USD": { "cash": "5314", "bank": "5124", "card": "5125.1" }, "EUR": { "cash": "", "bank": "", "card": "5125.3" } } }, "winmentor": { "bankName": "", "bankNumber": "", "bankLocality": "" }, "ciel": {} } ``` Missing keys fall back to the defaults shown above (in particular `accountCard` defaults to the SAGA-postable analytic `5125.2`, not the synthetic `5125`). `currencyAccounts` is an open-ended map keyed by ISO 4217 currency code; any empty per-currency field falls back to the corresponding RON account. The receipts/payments exporter splits the SAGA XML output by currency whenever non-RON payments exist (see [`POST /accounting-export/zip`](/api-reference/accounting-export/zip)). ## Update settings ``` PUT /api/v1/accounting-export/settings ``` ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Partial settings object — keys are deep-merged onto the existing configuration. ```json { "saga": { "accountCard": "5125.2" } } ``` ### Response Returns the new, fully-resolved settings (same shape as `GET`). ### Permissions - `GET` requires `settings.view`. - `PUT` requires `settings.manage`. --- ## Accounting export ZIP (SAGA) > Export SAGA-compatible XML files (clients, suppliers, products, invoices, receipts, payments) as a single ZIP archive URL: https://docs.storno.ro/api-reference/accounting-export/zip # Accounting export ZIP Builds a SAGA-compatible XML bundle (clients, suppliers, products, invoices, receipts, payments) and streams it back as a single ZIP archive. The response is the binary ZIP — no async job, no follow-up download endpoint. ``` POST /api/v1/accounting-export/zip ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `target` | string | Yes | Currently only `saga` is supported (`winmentor` and `ciel` return a friendly error). | | `dateFrom` | string | No | ISO date (`YYYY-MM-DD`) — start of the invoices/receipts/payments window. | | `dateTo` | string | No | ISO date (`YYYY-MM-DD`) — end of the window. | | `options` | object | No | Per-target options (see below). | ### SAGA options | Field | Type | Default | Description | |-------|------|---------|-------------| | `includeDiscount` | boolean | `false` | Emit invoice-level `` in `` when the invoice has a positive discount. | | `exportAccounts` | boolean | `true` | Also emit `conturi_cli_*.xml` and `conturi_frn_*.xml` (account assignment files). | | `exportBnr` | boolean | `false` | Include `curs_bnr_*.xml` with the current BNR FX rates. | | `accounts` | object | — | Per-export chart-of-accounts overrides. Missing keys fall back to the company’s stored SAGA settings; explicit values win for this export only. | | `currencyAccounts` | object | — | Per-currency overrides for foreign-currency receipts/payments. Each key is an upper-case ISO 4217 code (`USD`, `EUR`, …) and maps to `{ cash, bank, card }`. Merged on top of the stored `saga.currencyAccounts` setting. | ### `options.accounts` | Field | Default (stored setting) | Description | |-------|--------------------------|-------------| | `cash` | `5311` | Account used for cash receipts and cash supplier payments. | | `bank` | `5121` | Account used for bank transfers (OP). | | `card` | `5125.2` | Card analytic. **SAGA rejects a synthetic `5125`** — always use a leaf analytic such as `5125.2`. | | `clients` | `4111` | Used inside `conturi_cli_*.xml` when `exportAccounts` is true. | | `suppliers` | `4011` | Used inside `conturi_frn_*.xml` when `exportAccounts` is true. | ### `options.currencyAccounts` Map keyed by currency code: ```json "currencyAccounts": { "USD": { "cash": "5314", "bank": "5124", "card": "5125.1" }, "EUR": { "cash": "", "bank": "", "card": "5125.3" } } ``` Any individual field left empty falls back to the corresponding RON default (`accounts.cash` / `accounts.bank` / `accounts.card`). The map is merged on top of the stored `saga.currencyAccounts` setting, so per-export overrides only need to send the keys that change. ### Per-currency file splitting When receipts (or payments) span multiple currencies, the exporter splits them into one SAGA XML file per currency and uses the matching account map: - All-RON receipts → `inc_.xml` (legacy filename, unchanged). - Mixed RON/USD → `inc__RON.xml` + `inc__USD.xml`. Same rule for `plt_*.xml`. This is necessary because SAGA’s `` element has no currency tag — a single file is always imported under one currency analytic. ## Example request ```bash curl -X POST 'https://api.storno.ro/api/v1/accounting-export/zip' \ -H 'Authorization: Bearer ' \ -H 'X-Company: ' \ -H 'Content-Type: application/json' \ --output saga-export.zip \ -d '{ "target": "saga", "dateFrom": "2026-04-01", "dateTo": "2026-04-30", "options": { "includeDiscount": false, "exportAccounts": true, "exportBnr": false, "accounts": { "cash": "5311", "bank": "5121", "card": "5125.2", "clients": "4111", "suppliers": "4011" } } }' ``` ## Response Returns the ZIP archive directly: ```http Content-Type: application/zip Content-Disposition: attachment; filename="saga-export__.zip" ``` The archive contains: - `cli_.xml` — clients - `frn_.xml` — suppliers - `art_.xml` — products - `F__multiple_.xml` — outgoing invoices - `inc_.xml` — receipts (incasari) on outgoing invoices (or `inc__.xml` per currency when receipts span multiple currencies) - `plt_.xml` — payments (plati) on incoming invoices (or `plt__.xml` per currency) - `conturi_cli_.xml`, `conturi_frn_.xml` — when `exportAccounts=true` - `curs_bnr_.xml` — when `exportBnr=true` ## Notes on SAGA partner matching For card receipts/payments (`TipDocument=Card`), `` is always emitted empty — SAGA matches card flows through the merchant aggregator account, not a partner CIF. For cash and bank transfers, `` is emitted only when the invoice partner has a Romanian CIF/CUI (digits, with the optional `RO` prefix stripped). Foreign VAT identifiers (e.g. `DE...`, `BG...`, `FR...`) and internal client IDs are dropped to avoid SAGA rejecting the import line. ## Settings endpoints The default chart-of-accounts is stored per company at: - `GET /api/v1/accounting-export/settings` - `PUT /api/v1/accounting-export/settings` Use `options.accounts` here when you need to override those defaults for a single export without persisting a change. --- ## Admin Email Log > Query lifecycle email delivery records (SUPER_ADMIN only) URL: https://docs.storno.ro/api-reference/admin/email-log # Admin Email Log Query the lifecycle email delivery log. Records are written for every lifecycle email attempt regardless of outcome — sent, suppressed, or failed. Requires `ROLE_SUPER_ADMIN`. --- ## List Email Log Entries ```http GET /api/v1/admin/email-log ``` ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token (SUPER_ADMIN required) | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | page | integer | No | Page number (default: 1) | | limit | integer | No | Items per page (default: 25, max: 100) | | category | string | No | Filter by email category (see categories below) | | recipientEmail | string | No | Partial match on recipient email | | userId | string | No | Filter by associated user UUID | | status | string | No | Filter by status: `sent`, `failed`, `delivered`, `bounced` | | dateFrom | string | No | ISO 8601 date lower bound, inclusive (e.g. `2026-05-01`) | | dateTo | string | No | ISO 8601 date upper bound, inclusive (e.g. `2026-05-31`) | ### Email Categories | Category | Trigger | |----------|---------| | `re_engagement` | User inactive 14+ days | | `trial_expiration` | Trial ending in 7, 3, or 1 day | | `dunning` | Subscription past_due (attempt 2 at 3d, attempt 3 at 7d) | | `account_without_login` | Verified user with no login after 3 days | | `first_company_created` | First company added within last 24h | | `first_invoice_created` | First invoice issued within last 24h | | `trial_ended` | Trial ended, no active subscription (variants: `1d`, `7d`, `30d`) | | `feature_drip` | Trial-active org at day 3/7/10/14 of trial (variants: `efactura`, `anaf_lookup`, `contabil_user`, `mobile_app`) | ### Response ```json { "data": [ { "id": "01960000-0000-7000-8000-000000000001", "category": "trial_ended", "recipientEmail": "owner@example.com", "subject": "O saptamana de cand s-a terminat proba — mai esti interesat?", "status": "sent", "templateUsed": "7d", "errorMessage": null, "fromEmail": null, "sentBy": { "id": "01960000-0000-7000-8000-000000000002", "email": "owner@example.com" }, "sentAt": "2026-05-10T09:01:34+03:00" }, { "id": "01960000-0000-7000-8000-000000000003", "category": "feature_drip", "recipientEmail": "founder@company.ro", "subject": "e-Factura obligatorie — cum te ajuta Storno sa o rezolvi automat", "status": "sent", "templateUsed": "efactura", "errorMessage": null, "fromEmail": null, "sentBy": { "id": "01960000-0000-7000-8000-000000000004", "email": "founder@company.ro" }, "sentAt": "2026-05-10T09:00:12+03:00" } ], "total": 842, "page": 1, "limit": 25 } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | id | string | UUID of the log entry | | category | string | Email category | | recipientEmail | string | Recipient email address | | subject | string | Email subject line | | status | string | Delivery status: `sent`, `failed`, `delivered`, `bounced` | | templateUsed | string\|null | Variant identifier (e.g. `7d`, `efactura`, `skipped_gate`) | | errorMessage | string\|null | Error detail when status is `failed` | | fromEmail | string\|null | Sender address override if set | | sentBy | object\|null | Associated user (usually the recipient) | | sentAt | string | ISO 8601 timestamp | ### Status Values | Status | Meaning | |--------|---------| | `sent` | Email was accepted by the mailer (includes gate-suppressed rows where `templateUsed = skipped_gate`) | | `failed` | Mailer rejected the message or an exception occurred | | `delivered` | SES/mailer confirmed delivery (requires webhook integration) | | `bounced` | Hard or soft bounce reported by SES | ### Suppression Markers When the gate (`LifecycleEmailGate`) suppresses a send, the row is written with `status: sent` and `templateUsed: skipped_gate`. This preserves a full audit trail without conflating suppressions with genuine delivery. ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized | | 403 | Requires ROLE_SUPER_ADMIN | --- ## Admin Organization Management > Manage organizations (SUPER_ADMIN only) URL: https://docs.storno.ro/api-reference/admin/organizations # Admin Organization Management View and manage organizations with administrative privileges. This endpoint is restricted to SUPER_ADMIN users. --- ## List All Organizations ```http GET /api/v1/admin/organizations ``` Get a paginated, searchable list of all organizations on the platform. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token (SUPER_ADMIN required) | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | page | integer | No | Page number (default: 1) | | limit | integer | No | Items per page (default: 50, max: 200) | | search | string | No | Search by organization name or owner | | status | string | No | Filter by status: `active`, `trial`, `suspended` | | plan | string | No | Filter by plan type | ### Response ```json { "data": [ { "uuid": "550e8400-e29b-41d4-a716-446655440000", "name": "Acme Corporation SRL", "owner": { "uuid": "123e4567-e89b-12d3-a456-426614174000", "email": "john.doe@example.com", "name": "John Doe" }, "plan": { "name": "Professional", "status": "active", "expiresAt": "2027-01-15T00:00:00Z" }, "status": "active", "memberCount": 5, "companyCount": 2, "invoiceCount": 234, "hasValidAnafToken": true, "lastSyncAt": "2026-02-16T11:30:00Z", "createdAt": "2026-01-15T10:00:00Z" }, { "uuid": "660e8400-e29b-41d4-a716-446655440001", "name": "Beta Testing SRL", "owner": { "uuid": "223e4567-e89b-12d3-a456-426614174001", "email": "jane.smith@example.com", "name": "Jane Smith" }, "plan": { "name": "Free Trial", "status": "trial", "expiresAt": "2026-02-23T00:00:00Z" }, "status": "trial", "memberCount": 2, "companyCount": 1, "invoiceCount": 12, "hasValidAnafToken": false, "lastSyncAt": null, "createdAt": "2026-02-09T14:00:00Z" } ], "total": 456, "page": 1, "limit": 50, "pages": 10 } ``` ### Response Fields #### Pagination | Field | Type | Description | |-------|------|-------------| | data | array | Array of organization objects | | total | integer | Total organizations | | page | integer | Current page | | limit | integer | Items per page | | pages | integer | Total pages | #### Organization Object | Field | Type | Description | |-------|------|-------------| | uuid | string | Organization UUID | | name | string | Organization name | | owner | object | Owner details | | owner.uuid | string | Owner UUID | | owner.email | string | Owner email | | owner.name | string | Owner full name | | plan | object | Subscription plan details | | plan.name | string | Plan name | | plan.status | string | Plan status | | plan.expiresAt | string\|null | Plan expiration date | | status | string | Organization status | | memberCount | integer | Number of members | | companyCount | integer | Number of companies | | invoiceCount | integer | Total invoices | | hasValidAnafToken | boolean | Has valid ANAF token | | lastSyncAt | string\|null | Last sync timestamp | | createdAt | string | Creation timestamp | ### Status Values | Status | Description | |--------|-------------| | active | Active with valid subscription | | trial | On free trial period | | suspended | Account suspended | | expired | Subscription expired | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - requires SUPER_ADMIN role | | 422 | Invalid query parameters | ### Notes - Organizations are sorted by creation date (newest first) by default - Search searches both organization name and owner name/email - Statistics are calculated in real-time - Use pagination for performance with large datasets - All actions are logged for audit purposes --- ## Admin Platform Statistics > Get platform-wide statistics (SUPER_ADMIN only) URL: https://docs.storno.ro/api-reference/admin/stats # Admin Platform Statistics Get platform-wide statistics for monitoring and analytics. This endpoint is restricted to SUPER_ADMIN users. --- ## Get Platform Stats ```http GET /api/v1/admin/stats ``` Retrieve comprehensive platform statistics including user counts, organizations, and companies. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication (SUPER_ADMIN required) | ### Response ```json { "users": { "total": 1523, "active": 1402, "verified": 1450, "unverified": 73, "inactive": 121, "registeredToday": 12, "registeredThisWeek": 45, "registeredThisMonth": 187 }, "organizations": { "total": 456, "active": 423, "withPaidPlan": 234, "onTrial": 89, "suspended": 33, "createdToday": 3, "createdThisWeek": 18, "createdThisMonth": 67 }, "companies": { "total": 892, "withSync": 678, "withValidToken": 645, "syncedToday": 523, "syncedThisWeek": 712 }, "invoices": { "total": 45678, "thisMonth": 3456, "thisWeek": 892, "today": 123 }, "system": { "version": "1.2.3", "uptime": 3456789, "environment": "production" } } ``` ### Response Fields #### Users | Field | Type | Description | |-------|------|-------------| | total | integer | Total number of users | | active | integer | Active users (not suspended) | | verified | integer | Email-verified users | | unverified | integer | Unverified users | | inactive | integer | Inactive/suspended users | | registeredToday | integer | Registrations today | | registeredThisWeek | integer | Registrations this week | | registeredThisMonth | integer | Registrations this month | #### Organizations | Field | Type | Description | |-------|------|-------------| | total | integer | Total organizations | | active | integer | Active organizations | | withPaidPlan | integer | Organizations with paid plan | | onTrial | integer | Organizations on trial | | suspended | integer | Suspended organizations | | createdToday | integer | Created today | | createdThisWeek | integer | Created this week | | createdThisMonth | integer | Created this month | #### Companies | Field | Type | Description | |-------|------|-------------| | total | integer | Total companies | | withSync | integer | Companies with sync enabled | | withValidToken | integer | Companies with valid ANAF token | | syncedToday | integer | Companies synced today | | syncedThisWeek | integer | Companies synced this week | #### Invoices | Field | Type | Description | |-------|------|-------------| | total | integer | Total invoices in system | | thisMonth | integer | Invoices this month | | thisWeek | integer | Invoices this week | | today | integer | Invoices today | #### System | Field | Type | Description | |-------|------|-------------| | version | string | Application version | | uptime | integer | System uptime in seconds | | environment | string | Environment: production/staging/development | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - requires SUPER_ADMIN role | ### Notes - This endpoint is heavily cached (typically 5 minutes) - Statistics are approximations for large datasets - Only SUPER_ADMIN users can access this endpoint - Use for monitoring dashboards and analytics --- ## Admin User Management > Manage users (SUPER_ADMIN only) URL: https://docs.storno.ro/api-reference/admin/users # Admin User Management Manage user accounts with administrative privileges. These endpoints are restricted to SUPER_ADMIN users. --- ## List All Users ```http GET /api/v1/admin/users ``` Get a paginated, searchable list of all users on the platform. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token (SUPER_ADMIN required) | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | page | integer | No | Page number (default: 1) | | limit | integer | No | Items per page (default: 50, max: 200) | | search | string | No | Search by name or email | | status | string | No | Filter by status: `active`, `inactive`, `verified`, `unverified` | | role | string | No | Filter by role | ### Response ```json { "data": [ { "id": 123, "uuid": "123e4567-e89b-12d3-a456-426614174000", "email": "john.doe@example.com", "firstName": "John", "lastName": "Doe", "isActive": true, "isVerified": true, "role": "USER", "createdAt": "2026-01-15T10:00:00Z", "lastLoginAt": "2026-02-16T09:00:00Z", "organizations": [ { "uuid": "550e8400-e29b-41d4-a716-446655440000", "name": "Acme Corp", "role": "OWNER" } ] } ], "total": 1523, "page": 1, "limit": 50, "pages": 31 } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | id | integer | User database ID | | uuid | string | User UUID | | email | string | User email address | | firstName | string | First name | | lastName | string | Last name | | isActive | boolean | Account active status | | isVerified | boolean | Email verification status | | role | string | System role | | createdAt | string | ISO 8601 registration timestamp | | lastLoginAt | string\|null | ISO 8601 last login timestamp | | organizations | array | User's organizations and roles | --- ## Get User Details ```http GET /api/v1/admin/users/{id} ``` Get detailed information about a specific user. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | id | integer | User ID | ### Response ```json { "id": 123, "uuid": "123e4567-e89b-12d3-a456-426614174000", "email": "john.doe@example.com", "firstName": "John", "lastName": "Doe", "isActive": true, "isVerified": true, "role": "USER", "createdAt": "2026-01-15T10:00:00Z", "lastLoginAt": "2026-02-16T09:00:00Z", "emailVerifiedAt": "2026-01-15T10:05:00Z", "organizations": [ { "uuid": "550e8400-e29b-41d4-a716-446655440000", "name": "Acme Corp", "role": "OWNER", "joinedAt": "2026-01-15T10:00:00Z" } ], "devices": [ { "platform": "ios", "registeredAt": "2026-02-10T14:00:00Z" } ], "stats": { "invoiceCount": 234, "organizationCount": 1 } } ``` --- ## Toggle User Active Status ```http POST /api/v1/admin/users/{id}/toggle-active ``` Toggle a user's active status (activate/deactivate account). ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | id | integer | User ID | ### Response ```json { "id": 123, "isActive": false, "message": "User account deactivated" } ``` ### Notes - Deactivated users cannot log in - Existing sessions remain valid until expiry - Cannot deactivate SUPER_ADMIN users --- ## Verify User Email ```http POST /api/v1/admin/users/{id}/verify-email ``` Manually verify a user's email address. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | id | integer | User ID | ### Response ```json { "id": 123, "isVerified": true, "message": "User email verified" } ``` --- ## Resend Confirmation Email ```http POST /api/v1/admin/users/{id}/resend-confirmation ``` Resend the email confirmation link to a user. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | id | integer | User ID | ### Response ```json { "message": "Confirmation email sent" } ``` ### Error Responses All endpoints return: | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - requires SUPER_ADMIN role | | 404 | User not found | | 422 | Validation error | ### Notes - All user management actions are logged - Cannot modify other SUPER_ADMIN accounts - Email changes require re-verification - Use with caution - affects user access --- ## ANAF Connection Status > Check ANAF integration status and token validity URL: https://docs.storno.ro/api-reference/anaf/status # ANAF Connection Status Check the current status of ANAF integration for the authenticated user. --- ## Get ANAF Status ```http GET /api/v1/anaf/status ``` Retrieve ANAF connection status including token count and validity information. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response ```json { "tokenCount": 2, "hasValidToken": true, "tokens": [ { "cif": "12345678", "expiresAt": "2026-03-16T10:30:00Z", "isValid": true }, { "cif": "87654321", "expiresAt": "2026-01-10T08:00:00Z", "isValid": false } ] } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | tokenCount | integer | Total number of ANAF tokens saved | | hasValidToken | boolean | Whether user has at least one valid token | | tokens | array | Array of token status objects | | tokens[].cif | string | Company fiscal code | | tokens[].expiresAt | string | ISO 8601 expiry timestamp | | tokens[].isValid | boolean | Whether token is currently valid | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | --- ## ANAF Token Links > Device-based authentication flow for ANAF tokens URL: https://docs.storno.ro/api-reference/anaf/token-links # ANAF Token Links Create and manage token links for device-based ANAF authentication flow. --- ## Create Token Link ```http POST /api/v1/anaf/token-links ``` Create a token link for device-based authentication. This generates a unique link that can be used on mobile devices or other platforms to complete ANAF OAuth flow. Maximum of 5 active links per user are allowed. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response Returns `201 Created` with the token link details. ```json { "linkToken": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "url": "https://app.storno.ro/auth/anaf/device?token=a1b2c3d4-e5f6-7890-abcd-ef1234567890", "expiresAt": "2026-02-16T13:00:00Z" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | linkToken | string | Unique token identifier for this link | | url | string | Complete URL for device authentication | | expiresAt | string | ISO 8601 timestamp when link expires | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 429 | Too many active links (maximum 5) | --- ## Check Token Link Status ```http GET /api/v1/anaf/token-links/{linkToken} ``` Check the status of a token link to see if it has been used or expired. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | linkToken | string | The token link identifier | ### Response ```json { "status": "pending", "tokenId": null } ``` Or after use: ```json { "status": "used", "tokenId": 125 } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | status | string | Link status: `pending`, `used`, or `expired` | | tokenId | integer\|null | ANAF token ID (if status is `used`) | ### Status Values | Value | Description | |-------|-------------| | pending | Link is active and waiting to be used | | used | Link has been successfully used to add a token | | expired | Link has expired without being used | ### Error Responses | Status | Description | |--------|-------------| | 404 | Token link not found | --- ## ANAF Tokens > Manage ANAF OAuth tokens for e-Factura integration URL: https://docs.storno.ro/api-reference/anaf/tokens # ANAF Tokens Manage ANAF OAuth tokens that enable e-Factura synchronization for your companies. --- ## List ANAF Tokens ```http GET /api/v1/anaf/tokens ``` Retrieve all ANAF tokens associated with the authenticated user. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response Returns an array of ANAF token objects. ```json [ { "id": 123, "cif": "12345678", "expiresAt": "2026-03-16T10:30:00Z", "isValid": true, "createdAt": "2026-01-15T09:00:00Z" } ] ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | id | integer | Unique token identifier | | cif | string | Romanian company fiscal code (CIF) | | expiresAt | string | ISO 8601 timestamp when token expires | | isValid | boolean | Whether token is currently valid | | createdAt | string | ISO 8601 timestamp when token was created | --- ## Save New ANAF Token ```http POST /api/v1/anaf/tokens ``` Save a new ANAF OAuth access token. The token is validated with ANAF and JWT expiry is extracted. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | | Content-Type | string | Yes | Must be `application/json` | ### Request Body ```json { "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | token | string | Yes | The ANAF OAuth access token (JWT format) | ### Response Returns `201 Created` with the newly created token object. ```json { "id": 124, "cif": "12345678", "expiresAt": "2026-03-16T10:30:00Z", "isValid": true, "createdAt": "2026-02-16T12:00:00Z" } ``` ### Error Responses | Status | Description | |--------|-------------| | 400 | Invalid token format or expired token | | 401 | Unauthorized - invalid authentication | | 422 | Validation error - token already exists | --- ## Delete ANAF Token ```http DELETE /api/v1/anaf/tokens/{id} ``` Delete an ANAF token. This will disable e-Factura synchronization for the associated company. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | id | integer | The token ID to delete | ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response Returns `204 No Content` on successful deletion. ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - token belongs to another user | | 404 | Token not found | --- ## E-Factura Sync > Trigger and monitor e-Factura synchronization URL: https://docs.storno.ro/api-reference/anaf/sync # E-Factura Sync Manage e-Factura synchronization with ANAF SPV platform. --- ## Trigger Manual Sync ```http POST /api/v1/sync/trigger ``` Manually trigger e-Factura synchronization for all companies with valid ANAF tokens. The sync process: - Validates ANAF token availability - Checks subscription plan rate limits - Dispatches async sync job - Fetches new invoices from ANAF SPV ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response ```json { "message": "Sync triggered" } ``` ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - no valid ANAF token or rate limit exceeded | | 429 | Too many sync requests - try again later | --- ## Get Sync Status ```http GET /api/v1/sync/status ``` Get current synchronization status and configuration. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response ```json { "enabled": true, "lastSync": "2026-02-16T11:30:00Z", "tokenValid": true, "syncInterval": "hourly" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | enabled | boolean | Whether automatic sync is enabled | | lastSync | string\|null | ISO 8601 timestamp of last successful sync | | tokenValid | boolean | Whether user has a valid ANAF token | | syncInterval | string | Sync frequency: `hourly`, `daily`, or `manual` | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | --- ## Get Sync Log ```http GET /api/v1/sync/log ``` Retrieve recent sync activity log showing the last 50 synced invoices. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response Returns an array of recent sync activities. ```json [ { "invoiceId": "FINV2026001", "cif": "12345678", "syncedAt": "2026-02-16T11:30:15Z", "status": "success" }, { "invoiceId": "FINV2026002", "cif": "12345678", "syncedAt": "2026-02-16T11:30:18Z", "status": "success" } ] ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | invoiceId | string | Invoice identifier | | cif | string | Company CIF | | syncedAt | string | ISO 8601 timestamp of sync | | status | string | Sync status: `success`, `failed`, or `skipped` | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | --- ## Validate CIF > Validate ANAF token for a specific CIF URL: https://docs.storno.ro/api-reference/anaf/validate-cif # Validate CIF Validate that an ANAF token has proper access to e-Factura for a specific CIF. --- ## Validate Token for CIF ```http POST /api/v1/anaf/tokens/{id}/validate-cif ``` Validates the token by checking: - Organization ownership of the CIF - ANAF registry lookup - e-Factura access permissions ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | id | integer | The ANAF token ID to validate | ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response ```json { "valid": true, "errors": [] } ``` Or if validation fails: ```json { "valid": false, "errors": [ "CIF not found in ANAF registry", "Company does not have e-Factura access enabled" ] } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | valid | boolean | Whether validation passed | | errors | array | Array of error messages (empty if valid) | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - token belongs to another user | | 404 | Token not found | --- ## Create API token > Create a new API token for programmatic access to the API. URL: https://docs.storno.ro/api-reference/api-keys/create # Create API token Creates a new API token for the authenticated user. The raw token value is returned **only once** in the creation response and cannot be retrieved again. Store it securely immediately after creation. Requested scopes must be a subset of the permissions the authenticated user already holds. Attempting to grant scopes the user does not have will result in a validation error. ```http POST /api/v1/api-tokens ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `name` | string | Yes | A human-readable label for the token (e.g. "CI/CD Pipeline") | | `scopes` | string[] | Yes | One or more permission scope values. Must be valid `Permission` values and a subset of the user's own permissions. See [List available scopes](/api-reference/api-keys/scopes) | | `expiresAt` | string | No | ISO 8601 datetime at which the token expires. Omit for a non-expiring token | ## Response Returns the created token object with a `201 Created` status. The response includes a `token` field containing the raw token value. This field is **not** included in any subsequent response. ### Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier (UUID) | | `name` | string | Human-readable name | | `token` | string | The full raw token value — store this securely, it will not be shown again | | `tokenPrefix` | string | First 12 characters of the token, used for future identification | | `scopes` | string[] | Permission scopes granted to this token | | `lastUsedAt` | string \| null | Always `null` on creation | | `expireAt` | string \| null | ISO 8601 expiry timestamp, or `null` if the token never expires | | `revokedAt` | string \| null | Always `null` on creation | | `createdAt` | string | ISO 8601 creation timestamp | ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/api-tokens' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "CI/CD Pipeline", "scopes": ["invoice.view", "invoice.create", "client.view"], "expiresAt": "2027-01-01T00:00:00Z" }' ``` ## Example Response ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "CI/CD Pipeline", "token": "af_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3", "tokenPrefix": "af_a1b2c3d4e", "scopes": ["invoice.view", "invoice.create", "client.view"], "lastUsedAt": null, "expireAt": "2027-01-01T00:00:00Z", "revokedAt": null, "createdAt": "2026-02-18T10:00:00Z" } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - Missing `name` field - Missing or empty `scopes` array - One or more scope values are not valid `Permission` values - One or more scopes exceed the authenticated user's own permissions - `expiresAt` is in the past or has an invalid date format ## Important Notes - The `token` field in the response is the only time the raw token value is ever transmitted — it is stored as a one-way hash server-side - Tokens use the `af_` prefix to make them easily identifiable in source code and logs - There is no upper limit on the number of tokens a user can create, but each token is subject to the same rate limits as interactive sessions - Tokens inherit the organization context from the user who created them ## Related Endpoints - [List API tokens](/api-reference/api-keys/list) - [Update API token](/api-reference/api-keys/update) - [Revoke API token](/api-reference/api-keys/revoke) - [List available scopes](/api-reference/api-keys/scopes) --- ## List API tokens > Retrieve all API tokens for the current user within the current organization. URL: https://docs.storno.ro/api-reference/api-keys/list # List API tokens Retrieves all API tokens belonging to the authenticated user within the current organization. Tokens are returned sorted by creation date, newest first. The raw token value is never included in this response — it is only available once at creation time. ```http GET /api/v1/api-tokens ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | ## Response Returns an array of API token objects sorted by `createdAt` descending. ### API Token Object | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier (UUID) | | `name` | string | Human-readable name given to the token | | `tokenPrefix` | string | First 12 characters of the token, used for identification | | `scopes` | string[] | Array of permission scopes granted to this token | | `lastUsedAt` | string \| null | ISO 8601 timestamp of the most recent successful use, or `null` if never used | | `expireAt` | string \| null | ISO 8601 expiry timestamp, or `null` if the token never expires | | `revokedAt` | string \| null | ISO 8601 revocation timestamp, or `null` if the token is still active | | `createdAt` | string | ISO 8601 creation timestamp | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/api-tokens' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` ## Example Response ```json [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "CI/CD Pipeline", "tokenPrefix": "af_a1b2c3d4e", "scopes": ["invoice.view", "invoice.create", "client.view"], "lastUsedAt": "2026-02-17T11:42:00Z", "expireAt": "2027-01-01T00:00:00Z", "revokedAt": null, "createdAt": "2026-01-15T09:00:00Z" }, { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "name": "Accounting Export Script", "tokenPrefix": "af_b2c3d4e5f", "scopes": ["invoice.view", "export.data"], "lastUsedAt": null, "expireAt": null, "revokedAt": "2026-02-10T08:00:00Z", "createdAt": "2025-11-20T14:30:00Z" } ] ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | ## Important Notes - The `tokenHash` field is never returned in any list or detail response - Both active and revoked tokens are included in the response; filter on `revokedAt` to show only active tokens - Tokens are scoped to the authenticated user — other users' tokens are never visible - The `tokenPrefix` is sufficient to let a user identify which token is which without exposing the secret ## Related Endpoints - [Create API token](/api-reference/api-keys/create) - [Update API token](/api-reference/api-keys/update) - [Revoke API token](/api-reference/api-keys/revoke) - [List available scopes](/api-reference/api-keys/scopes) --- ## List available scopes > Retrieve all permission scopes available to the current user, grouped by category. URL: https://docs.storno.ro/api-reference/api-keys/scopes # List available scopes Returns all permission scopes the authenticated user is eligible to grant to an API token. Only scopes the user themselves holds are returned — this endpoint cannot be used to discover scopes beyond the user's own permission set. The response is intended to power the scope picker in token creation and editing UIs. ```http GET /api/v1/api-tokens/scopes ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | ## Response Returns an object containing a `scopes` array. Each item represents a single grantable permission. ### Response Fields | Field | Type | Description | |-------|------|-------------| | `scopes` | object[] | Array of available scope objects | | `scopes[].value` | string | The scope identifier used in API requests (e.g. `"invoice.view"`) | | `scopes[].label` | string | Human-readable display label, identical to `value` | | `scopes[].category` | string | The resource category this scope belongs to (e.g. `"invoice"`) | ### Scope Categories and Values | Category | Scope Values | |----------|-------------| | `company` | `company.view`, `company.create`, `company.edit`, `company.delete` | | `client` | `client.view`, `client.create`, `client.edit`, `client.delete` | | `product` | `product.view`, `product.create`, `product.edit`, `product.delete` | | `invoice` | `invoice.view`, `invoice.create`, `invoice.edit`, `invoice.delete`, `invoice.issue`, `invoice.send`, `invoice.cancel`, `invoice.refund` | | `series` | `series.view`, `series.manage` | | `payment` | `payment.view`, `payment.create`, `payment.delete` | | `efactura` | `efactura.view`, `efactura.submit` | | `settings` | `settings.view`, `settings.manage` | | `org` | `org.manage_members`, `org.manage_billing`, `org.view_audit` | | `export` | `export.data` | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/api-tokens/scopes' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` ## Example Response ```json { "scopes": [ { "value": "company.view", "label": "company.view", "category": "company" }, { "value": "company.create", "label": "company.create", "category": "company" }, { "value": "company.edit", "label": "company.edit", "category": "company" }, { "value": "company.delete", "label": "company.delete", "category": "company" }, { "value": "client.view", "label": "client.view", "category": "client" }, { "value": "client.create", "label": "client.create", "category": "client" }, { "value": "client.edit", "label": "client.edit", "category": "client" }, { "value": "client.delete", "label": "client.delete", "category": "client" }, { "value": "invoice.view", "label": "invoice.view", "category": "invoice" }, { "value": "invoice.create", "label": "invoice.create", "category": "invoice" }, { "value": "invoice.edit", "label": "invoice.edit", "category": "invoice" }, { "value": "invoice.delete", "label": "invoice.delete", "category": "invoice" }, { "value": "invoice.issue", "label": "invoice.issue", "category": "invoice" }, { "value": "invoice.send", "label": "invoice.send", "category": "invoice" }, { "value": "invoice.cancel", "label": "invoice.cancel", "category": "invoice" }, { "value": "invoice.refund", "label": "invoice.refund", "category": "invoice" }, { "value": "export.data", "label": "export.data", "category": "export" } ] } ``` > The example above shows a partial response for a user who does not hold all possible scopes. A user with full administrator permissions would receive all scopes across every category. ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | ## Important Notes - The list of returned scopes is filtered to the authenticated user's own permissions — use this endpoint to build the scope picker rather than hardcoding scope lists in the client - Scopes not present in this response will be rejected with a `422` error if provided to [Create API token](/api-reference/api-keys/create) or [Update API token](/api-reference/api-keys/update) - The `label` field is currently equal to `value`; a future version may include localized display strings - Results are not paginated — the full list is always returned in a single response ## Related Endpoints - [List API tokens](/api-reference/api-keys/list) - [Create API token](/api-reference/api-keys/create) - [Update API token](/api-reference/api-keys/update) - [Revoke API token](/api-reference/api-keys/revoke) --- ## Revoke API token > Revoke an API token, immediately preventing it from being used for authentication. URL: https://docs.storno.ro/api-reference/api-keys/revoke # Revoke API token Revokes an existing API token by recording a `revokedAt` timestamp on the record. Revocation is a soft operation — the token row is not hard-deleted from the database — but the token immediately stops working for authentication. Only the token's owner can perform this operation. ```http DELETE /api/v1/api-tokens/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the API token to revoke | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | ## Response Returns a `204 No Content` status with no response body on success. ## Example Request ```bash curl -X DELETE 'https://api.storno.ro/api/v1/api-tokens/a1b2c3d4-e5f6-7890-abcd-ef1234567890' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` ## Example Response ```http HTTP/1.1 204 No Content ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | The authenticated user is not the owner of this token | | 404 | not_found | API token not found | ## Important Notes - Revocation takes effect immediately — any in-flight request using the revoked token after this call returns will receive a `401 Unauthorized` response - Revocation is permanent and cannot be undone; create a new token if access needs to be re-established - The revoked token remains visible in the [List API tokens](/api-reference/api-keys/list) response with a non-null `revokedAt` value, providing an audit trail - Revoking a token that is already revoked returns `204 No Content` without error, making this operation idempotent - A user cannot revoke their own current session token via this endpoint; that token is a JWT, not an API token ## Related Endpoints - [List API tokens](/api-reference/api-keys/list) - [Create API token](/api-reference/api-keys/create) - [Update API token](/api-reference/api-keys/update) - [List available scopes](/api-reference/api-keys/scopes) --- ## Update API token > Update the name or scopes of an existing API token. URL: https://docs.storno.ro/api-reference/api-keys/update # Update API token Updates the `name` or `scopes` of an existing API token. Only the token's owner can perform this operation. The token value itself and the expiry date cannot be changed — revoke and recreate the token if a new expiry is needed. ```http PATCH /api/v1/api-tokens/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the API token to update | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters All parameters are optional, but at least one must be provided. | Parameter | Type | Description | |-----------|------|-------------| | `name` | string | New human-readable label for the token | | `scopes` | string[] | Replacement set of permission scope values. Must be valid `Permission` values and a subset of the authenticated user's own permissions. Replaces the entire existing scope list | ## Response Returns the updated API token object with a `200 OK` status. The raw token value is never included in this response. ### Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier (UUID) | | `name` | string | Updated human-readable name | | `tokenPrefix` | string | First 12 characters of the token | | `scopes` | string[] | Updated permission scopes | | `lastUsedAt` | string \| null | ISO 8601 timestamp of the most recent successful use | | `expireAt` | string \| null | ISO 8601 expiry timestamp, unchanged by this operation | | `revokedAt` | string \| null | ISO 8601 revocation timestamp, or `null` if still active | | `createdAt` | string | ISO 8601 creation timestamp | ## Example Request ```bash curl -X PATCH 'https://api.storno.ro/api/v1/api-tokens/a1b2c3d4-e5f6-7890-abcd-ef1234567890' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "CI/CD Pipeline (read-only)", "scopes": ["invoice.view", "client.view"] }' ``` ## Example Response ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "CI/CD Pipeline (read-only)", "tokenPrefix": "af_a1b2c3d4e", "scopes": ["invoice.view", "client.view"], "lastUsedAt": "2026-02-17T11:42:00Z", "expireAt": "2027-01-01T00:00:00Z", "revokedAt": null, "createdAt": "2026-01-15T09:00:00Z" } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | The authenticated user is not the owner of this token | | 404 | not_found | API token not found | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - No fields provided for update - One or more scope values are not valid `Permission` values - One or more scopes exceed the authenticated user's own permissions - `name` is an empty string ## Important Notes - Updating `scopes` replaces the full scope list — it is not an additive operation. Send the complete desired set of scopes - The `expireAt` value cannot be modified via this endpoint; revoke the token and create a new one with the desired expiry - The raw token value is never returned — only `tokenPrefix` is exposed for identification - Updating a revoked token's name or scopes is permitted but has no effect on authentication since revoked tokens are always rejected ## Related Endpoints - [List API tokens](/api-reference/api-keys/list) - [Create API token](/api-reference/api-keys/create) - [Revoke API token](/api-reference/api-keys/revoke) - [List available scopes](/api-reference/api-keys/scopes) --- ## Disable TOTP > Disable two-factor authentication for the current user URL: https://docs.storno.ro/api-reference/auth/mfa-totp-disable # Disable TOTP Disable TOTP-based two-factor authentication. Requires password confirmation for security. Deletes the TOTP secret and all remaining backup codes. ## Request ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `password` | string | Yes | User's current account password | ### Example Request ```bash curl -X POST https://api.storno.ro/api/v1/me/mfa/totp/disable \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "password": "your-password" }' ``` ```js const response = await fetch('https://api.storno.ro/api/v1/me/mfa/totp/disable', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ password: 'your-password' }), }); ``` ## Response ### Success Response (200 OK) ```json { "disabled": true } ``` ## Error Codes | Code | Description | |------|-------------| | `401` | Unauthorized — missing or invalid JWT token | | `422` | Invalid password | --- ## Email Confirmation > Confirm user email address and resend confirmation emails URL: https://docs.storno.ro/api-reference/auth/confirm-email # Email Confirmation Verify user email addresses through confirmation tokens sent via email. Email confirmation may be required before accessing certain features or logging in. --- ## Confirm Email Verify a user's email address using a confirmation token sent during registration. **Endpoint**: `POST /api/auth/confirm-email` **Authentication**: Not required (public endpoint) ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `token` | string | Yes | Email confirmation token from verification email | ### Request ```bash curl -X POST https://api.storno.ro/api/auth/confirm-email \ -H "Content-Type: application/json" \ -d '{ "token": "abc123xyz789def456ghi..." }' ``` ```js // Extract token from URL const urlParams = new URLSearchParams(window.location.search); const token = urlParams.get('token'); const response = await fetch('https://api.storno.ro/api/auth/confirm-email', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: token, }), }); if (response.ok) { window.location.href = '/login?confirmed=true'; } ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->post('https://api.storno.ro/api/auth/confirm-email', [ 'json' => [ 'token' => $_GET['token'], ], ]); if ($response->getStatusCode() === 200) { header('Location: /login?confirmed=true'); exit; } ``` ### Response #### Success Response (200 OK) ```json { "message": "Email confirmed successfully. You can now log in to your account." } ``` ### Error Codes | Code | Description | |------|-------------| | `400` | Bad Request - Missing or invalid token format | | `401` | Unauthorized - Token is invalid, expired, or already used | | `409` | Conflict - Email is already confirmed | ### Error Response Examples **Invalid or Expired Token (401)** ```json { "code": 401, "message": "Email confirmation token is invalid or has expired." } ``` **Email Already Confirmed (409)** ```json { "code": 409, "message": "This email address has already been confirmed." } ``` **Token Already Used (401)** ```json { "code": 401, "message": "This confirmation token has already been used." } ``` --- ## Resend Confirmation Email Request a new confirmation email if the original was not received or has expired. **Endpoint**: `POST /api/auth/resend-confirmation` **Authentication**: Not required (public endpoint) ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `email` | string | Yes | Email address of the account | ### Request ```bash curl -X POST https://api.storno.ro/api/auth/resend-confirmation \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com" }' ``` ```js const response = await fetch('https://api.storno.ro/api/auth/resend-confirmation', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'user@example.com', }), }); if (response.ok) { console.log('Confirmation email sent (if account exists and is unconfirmed)'); } ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->post('https://api.storno.ro/api/auth/resend-confirmation', [ 'json' => [ 'email' => 'user@example.com', ], ]); ``` ### Response #### Success Response (200 OK) For security reasons, the endpoint always returns success regardless of whether the email exists or is already confirmed. ```json { "message": "If an unconfirmed account with that email exists, a new confirmation email has been sent." } ``` ### Error Codes | Code | Description | |------|-------------| | `400` | Bad Request - Invalid email format | | `429` | Too Many Requests - Rate limit exceeded | ### Error Response Examples **Invalid Email Format (400)** ```json { "code": 400, "message": "Validation failed", "errors": { "email": ["This value is not a valid email address."] } } ``` **Rate Limit Exceeded (429)** ```json { "code": 429, "message": "Too many confirmation email requests. Please try again in 5 minutes." } ``` --- ## Confirmation Email When a user registers or requests a resend, they receive an email containing: - A welcome message - A confirmation link with embedded token - Instructions on what to do if they didn't register - Link expiration time (24 hours) **Example Confirmation Email:** ``` Subject: Confirm Your Storno.ro Email Address Hi John, Welcome to Storno.ro! Please confirm your email address by clicking the link below: https://storno.ro/confirm-email?token=abc123xyz789def456ghi... This link will expire in 24 hours. If you didn't create an account with Storno.ro, you can safely ignore this email. Thanks, The Storno.ro Team ``` --- ## Token Expiration Confirmation tokens have specific lifecycle: - **Validity**: 24 hours from issuance - **Single-use**: Token is invalidated after successful confirmation - **Renewable**: User can request new token via resend endpoint - **Automatic cleanup**: Expired tokens are periodically removed --- ## Rate Limiting To prevent abuse, confirmation email requests are rate-limited: - **Per Email**: Maximum 3 requests per hour per email address - **Per IP**: Maximum 10 requests per hour per IP address - **Cooldown**: 5 minutes between requests for the same email --- ## Complete User Flow ### Registration with Email Confirmation 1. **User Registers** ```js await fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'user@example.com', password: 'SecurePass123!', firstName: 'John', lastName: 'Doe', }), }); ``` 2. **System Sends Confirmation Email** - User receives email with confirmation link - Link contains unique token 3. **User Clicks Confirmation Link** - Browser opens: `https://storno.ro/confirm-email?token=abc123xyz...` - Frontend extracts token and calls API 4. **Frontend Confirms Email** ```js const token = new URLSearchParams(window.location.search).get('token'); await fetch('/api/auth/confirm-email', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token }), }); ``` 5. **User Can Now Login** - Redirect to login page with success message - User authenticates with confirmed account --- ## Implementation Examples ### Email Confirmation Page ```html

Confirming your email address...

``` ### Resend Confirmation Form ```html

Resend Confirmation Email

Didn't receive the confirmation email? Enter your email address below.

``` --- ## Configuration Options Email confirmation requirements can vary based on system configuration: ### Optional Confirmation - Users can login immediately after registration - Email confirmation unlocks additional features - Unconfirmed users may have limited access ### Required Confirmation - Users must confirm email before first login - Login attempts fail with "Email not confirmed" error - Stronger security for sensitive applications --- ## Troubleshooting ### User Didn't Receive Confirmation Email 1. **Check spam/junk folder** 2. **Verify email address is correct** 3. **Wait a few minutes** (email delivery can be delayed) 4. **Use resend confirmation** endpoint 5. **Contact support** if issue persists ### Confirmation Link Expired 1. **Request new confirmation** via resend endpoint 2. **Complete confirmation within 24 hours** ### Token Invalid or Already Used 1. **Email already confirmed** - User can login directly 2. **Token malformed** - Request new confirmation email 3. **Wrong account** - Use correct email address --- ## Security Considerations - Tokens are cryptographically secure random strings - Tokens are hashed before database storage - Rate limiting prevents enumeration attacks - Generic success messages prevent email discovery - All confirmation attempts are logged for audit - Expired tokens are automatically cleaned up - One-time use prevents replay attacks --- ## Related Endpoints - [Register](/api-reference/auth/register) - Create account (triggers confirmation email) - [Login](/api-reference/auth/login) - Authenticate after confirmation - [User Profile](/api-reference/auth/me) - Check email confirmation status --- ## Best Practices 1. **Clear Communication** - Inform users to check email during registration - Provide resend option prominently - Explain what email confirmation unlocks 2. **User Experience** - Auto-confirm when user clicks link - Show loading state during confirmation - Redirect to login after success - Handle already-confirmed state gracefully 3. **Error Handling** - Display helpful error messages - Offer resend option on error - Log errors for monitoring 4. **Email Deliverability** - Use verified sender domain - Include text-only version - Clear subject line - Avoid spam triggers in content --- ## Enable TOTP > Verify a TOTP code to activate two-factor authentication URL: https://docs.storno.ro/api-reference/auth/mfa-totp-enable # Enable TOTP Verify a TOTP code from the user's authenticator app to activate two-factor authentication. On success, generates 10 single-use backup codes that should be stored securely. Must be called after [Setup TOTP](/api-reference/auth/mfa-totp-setup). ## Request ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `code` | string | Yes | 6-digit TOTP code from authenticator app | ### Example Request ```bash curl -X POST https://api.storno.ro/api/v1/me/mfa/totp/enable \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "code": "123456" }' ``` ```js const response = await fetch('https://api.storno.ro/api/v1/me/mfa/totp/enable', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ code: '123456' }), }); const { enabled, backupCodes } = await response.json(); ``` ## Response ### Success Response (200 OK) ```json { "enabled": true, "backupCodes": [ "a3km-v7np", "h2bx-q9wt", "f4jy-m6cr", "d8ns-w3gp", "k5ht-b2xv", "p7mf-j4qs", "r9cw-n6yd", "t2gv-k8hb", "v6xp-f3mt", "w4qn-s7jc" ] } ``` | Field | Type | Description | |-------|------|-------------| | `enabled` | boolean | Always `true` on success | | `backupCodes` | string[] | Array of 10 single-use backup codes (format: `xxxx-xxxx`) | Backup codes are shown **only once**. Prompt the user to save them securely — they cannot be retrieved later. Each code can only be used once. ## Error Codes | Code | Description | |------|-------------| | `400` | Missing `code` parameter | | `401` | Unauthorized — missing or invalid JWT token | | `409` | Conflict — TOTP is already enabled | | `422` | Invalid TOTP code | ## Usage Notes - The TOTP code is validated with a window of ±1 time step (allows 30 seconds of clock drift) - After enabling, all future email/password and Google OAuth logins will require a second factor - Passkey logins are **not** affected — passkeys inherently satisfy multi-factor requirements - Backup codes use only unambiguous characters (`abcdefghjkmnpqrstuvwxyz23456789`) --- ## Forgot Password > Request a password reset email URL: https://docs.storno.ro/api-reference/auth/forgot-password # Forgot Password Request a password reset email. If the email address exists in the system, a secure reset link will be sent. ## Request ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `email` | string | Yes | Email address associated with the account | ### Example Request ```bash curl -X POST https://api.storno.ro/api/auth/forgot-password \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com" }' ``` ```js const response = await fetch('https://api.storno.ro/api/auth/forgot-password', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'user@example.com', }), }); if (response.ok) { console.log('Password reset email sent (if account exists)'); } ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->post('https://api.storno.ro/api/auth/forgot-password', [ 'json' => [ 'email' => 'user@example.com', ], ]); // Always returns 200, even if email doesn't exist ``` ## Response ### Success Response (200 OK) The endpoint always returns a success response, regardless of whether the email exists. This prevents user enumeration attacks. ```json { "message": "If an account with that email exists, a password reset link has been sent." } ``` ## Security Behavior For security reasons, this endpoint: 1. **Always returns 200 OK** - Never reveals whether an email exists in the system 2. **Sends email only if account exists** - No email is sent for non-existent accounts 3. **Rate limits aggressively** - Prevents abuse and email bombing ## Password Reset Email If the account exists, the user receives an email containing: - A secure reset link valid for 1 hour - Instructions on how to reset their password - A warning that they didn't request this (if it wasn't them) - Link to contact support if needed **Example Email:** ``` Subject: Reset Your Storno.ro Password Hi John, You recently requested to reset your password for your Storno.ro account. Click the link below to reset it: https://storno.ro/reset-password?token=abc123xyz... This link will expire in 1 hour. If you didn't request a password reset, you can safely ignore this email. Your password will not be changed. Need help? Contact us at support@storno.ro Thanks, The Storno.ro Team ``` ## Error Codes | Code | Description | |------|-------------| | `400` | Bad Request - Missing or invalid email address | | `429` | Too Many Requests - Rate limit exceeded | ### Error Response Examples **Invalid Email Format (400)** ```json { "code": 400, "message": "Validation failed", "errors": { "email": ["This value is not a valid email address."] } } ``` **Rate Limit Exceeded (429)** ```json { "code": 429, "message": "Too many password reset requests. Please try again in 15 minutes." } ``` ## Rate Limiting To prevent abuse, password reset requests are rate-limited: - **Per Email**: Maximum 3 requests per hour per email address - **Per IP**: Maximum 10 requests per hour per IP address - **Cooldown**: 15 minutes between requests for the same email ## Usage Notes ### User Flow 1. User clicks "Forgot Password" on login page 2. User enters their email address 3. User submits the form 4. System shows generic success message 5. User checks email for reset link 6. User clicks link and is redirected to password reset page 7. User enters new password and confirms 8. User is redirected to login page with success message ### Implementation Example ```js // Forgot password form handler async function handleForgotPassword(email) { try { const response = await fetch('/api/auth/forgot-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); if (response.ok) { showMessage( 'If an account with that email exists, we\'ve sent password reset instructions.', 'success' ); } } catch (error) { showMessage('An error occurred. Please try again later.', 'error'); } } ``` ### Best Practices 1. **Generic messaging** - Never reveal if an email exists or not 2. **Clear instructions** - Tell users to check their email (including spam folder) 3. **Expiry notice** - Inform users the link expires in 1 hour 4. **Alternative options** - Provide support contact for users who can't access email 5. **Resend option** - Allow users to request another reset email after cooldown ### Security Considerations - Reset tokens are single-use and expire after 1 hour - Tokens are cryptographically secure random strings - Using a reset token invalidates any previous tokens - Successfully resetting password invalidates all existing sessions - All password reset attempts are logged for security auditing ## Related Endpoints - [Reset Password](/api-reference/auth/reset-password) - Complete the password reset with token - [Login](/api-reference/auth/login) - Authenticate after resetting password - [Register](/api-reference/auth/register) - Create a new account if you don't have one ## Troubleshooting **User didn't receive email:** 1. Check spam/junk folder 2. Verify email address is correct 3. Wait a few minutes (email delivery can be delayed) 4. Try requesting another reset after cooldown period 5. Contact support if issue persists **Reset link expired:** 1. Request a new password reset 2. Complete the process within 1 hour **Multiple reset requests:** - Only the most recent token is valid - Previous tokens are automatically invalidated --- ## Google OAuth Login > Authenticate or register using Google OAuth URL: https://docs.storno.ro/api-reference/auth/google-oauth # Google OAuth Login Authenticate or register a user using Google OAuth. If the user doesn't exist, a new account is automatically created. ## Request ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `idToken` | string | Yes | Google ID token from Google Sign-In | ### Example Request ```bash curl -X POST https://api.storno.ro/api/auth/google \ -H "Content-Type: application/json" \ -d '{ "idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5ZmUyYTdiNjc5NTIzOTYwNmNhMGE3NTA3N..." }' ``` ```js // After Google Sign-In const googleUser = await google.accounts.id.prompt(); const idToken = googleUser.credential; const response = await fetch('https://api.storno.ro/api/auth/google', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ idToken: idToken, }), }); const { token, refresh_token, isNewUser } = await response.json(); ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->post('https://api.storno.ro/api/auth/google', [ 'json' => [ 'idToken' => $googleIdToken, ], ]); $data = json_decode($response->getBody(), true); $token = $data['token']; $refreshToken = $data['refresh_token']; $isNewUser = $data['isNewUser']; ``` ## Response ### Success Response (200 OK) ```json { "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "def50200a1b2c3d4e5f6...", "isNewUser": false } ``` | Field | Type | Description | |-------|------|-------------| | `token` | string | JWT access token, valid for 1 hour | | `refresh_token` | string | Refresh token used to obtain new access tokens | | `isNewUser` | boolean | `true` if account was just created, `false` for existing user | ## Error Codes | Code | Description | |------|-------------| | `400` | Bad Request - Missing or invalid ID token | | `401` | Unauthorized - Invalid Google ID token or token verification failed | | `403` | Forbidden - Google account email not verified | | `429` | Too Many Requests - Rate limit exceeded | ### Error Response Examples **Invalid ID Token (401)** ```json { "code": 401, "message": "Invalid Google ID token." } ``` **Email Not Verified (403)** ```json { "code": 403, "message": "Google account email must be verified." } ``` ## Automatic Account Creation When a user signs in with Google for the first time: 1. **Email Verification**: The system verifies the Google ID token with Google's servers 2. **User Lookup**: Checks if a user with the Google email already exists 3. **Auto-Registration**: If not found, creates a new user account with: - Email from Google account - First and last name from Google profile - Email marked as confirmed (no confirmation email needed) - Default organization created with owner role 4. **Token Issuance**: Returns JWT tokens for immediate authenticated access ## Integration with Google Sign-In ### Frontend Setup (HTML/JavaScript) ```html
``` ### Mobile Setup (React Native) ```javascript import { GoogleSignin } from '@react-native-google-signin/google-signin'; // Configure Google Sign-In GoogleSignin.configure({ webClientId: 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com', }); // Sign in function async function signInWithGoogle() { try { await GoogleSignin.hasPlayServices(); const userInfo = await GoogleSignin.signIn(); const idToken = userInfo.idToken; const response = await fetch('https://api.storno.ro/api/auth/google', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ idToken }), }); const data = await response.json(); // Store tokens and navigate } catch (error) { console.error('Google Sign-In failed:', error); } } ``` ### MFA Challenge Response (200 OK) If the user has two-factor authentication enabled, the endpoint returns an MFA challenge instead of tokens: ```json { "mfa_required": true, "mfa_token": "a1b2c3d4e5f6789...", "mfa_methods": ["totp", "backup_code"] } ``` Complete the challenge by calling [Verify MFA Challenge](/api-reference/auth/mfa-verify) with the `mfa_token` and a valid code. ## Usage Notes - Google ID tokens are verified server-side for security - When the user has MFA enabled, handle the `mfa_required` response by redirecting to MFA verification - Token verification includes checking signature, expiration, and audience - Users can link multiple authentication methods (password + Google) to the same email - Google OAuth does not require a separate password - Email confirmation is not required for Google OAuth users - Rate limiting: maximum 10 OAuth attempts per minute per IP address ## Security Considerations - Always validate ID tokens on the server side - Never trust client-side token validation alone - ID tokens expire quickly (typically 1 hour) - Store Google ID tokens securely and never expose them in URLs - Use HTTPS for all authentication requests --- ## Login > Authenticate a user with email and password URL: https://docs.storno.ro/api-reference/auth/login # Login Authenticate a user with their email and password to receive JWT access and refresh tokens. ## Request ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `email` | string | Yes | User's email address | | `password` | string | Yes | User's password | ### Example Request ```bash curl -X POST https://api.storno.ro/api/auth \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "password": "your-secure-password" }' ``` ```js const response = await fetch('https://api.storno.ro/api/auth', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'user@example.com', password: 'your-secure-password', }), }); const { token, refresh_token } = await response.json(); ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->post('https://api.storno.ro/api/auth', [ 'json' => [ 'email' => 'user@example.com', 'password' => 'your-secure-password', ], ]); $data = json_decode($response->getBody(), true); $token = $data['token']; $refreshToken = $data['refresh_token']; ``` ## Response ### Success Response (200 OK) ```json { "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "def50200a1b2c3d4e5f6..." } ``` | Field | Type | Description | |-------|------|-------------| | `token` | string | JWT access token, valid for 1 hour | | `refresh_token` | string | Refresh token used to obtain new access tokens | ## Error Codes | Code | Description | |------|-------------| | `400` | Bad Request - Missing or invalid parameters | | `401` | Unauthorized - Invalid email or password | | `403` | Forbidden - Account is inactive or email not confirmed | | `429` | Too Many Requests - Rate limit exceeded | ### Error Response Examples **Invalid Credentials (401)** ```json { "code": 401, "message": "Invalid credentials." } ``` **Email Not Confirmed (403)** ```json { "code": 403, "message": "Please confirm your email address before logging in." } ``` **Account Inactive (403)** ```json { "code": 403, "message": "Your account has been deactivated. Please contact support." } ``` ### MFA Challenge Response (200 OK) If the user has two-factor authentication enabled, the login endpoint returns an MFA challenge instead of tokens: ```json { "mfa_required": true, "mfa_token": "a1b2c3d4e5f6789...", "mfa_methods": ["totp", "backup_code"] } ``` | Field | Type | Description | |-------|------|-------------| | `mfa_required` | boolean | Always `true` when MFA is needed | | `mfa_token` | string | 64-character challenge token (valid for 5 minutes) | | `mfa_methods` | string[] | Available verification methods: `totp` and/or `backup_code` | Complete the challenge by calling [Verify MFA Challenge](/api-reference/auth/mfa-verify) with the `mfa_token` and a valid code. ## Usage Notes - Store the `token` securely (e.g., in memory or secure storage) - Include the token in subsequent requests via the `Authorization: Bearer {token}` header - Use the `refresh_token` to obtain a new access token when it expires - Tokens are rotated on refresh for enhanced security - Rate limiting applies: maximum 5 login attempts per minute per IP address - When the user has MFA enabled, handle the `mfa_required` response by redirecting to MFA verification --- ## MFA Status > Get multi-factor authentication status for the current user URL: https://docs.storno.ro/api-reference/auth/mfa-status # MFA Status Retrieve the current MFA configuration for the authenticated user, including whether TOTP is enabled, remaining backup codes, and registered passkeys count. ## Request No request body required. ### Example Request ```bash curl https://api.storno.ro/api/v1/me/mfa/status \ -H "Authorization: Bearer {token}" ``` ```js const response = await fetch('https://api.storno.ro/api/v1/me/mfa/status', { headers: { 'Authorization': `Bearer ${token}` }, }); const status = await response.json(); ``` ## Response ### Success Response (200 OK) ```json { "totpEnabled": true, "backupCodesRemaining": 8, "passkeysCount": 2 } ``` | Field | Type | Description | |-------|------|-------------| | `totpEnabled` | boolean | Whether TOTP-based 2FA is enabled | | `backupCodesRemaining` | integer | Number of unused backup codes remaining | | `passkeysCount` | integer | Number of registered WebAuthn passkeys | ## Error Codes | Code | Description | |------|-------------| | `401` | Unauthorized — missing or invalid JWT token | --- ## Passkeys (WebAuthn) > Passwordless authentication using WebAuthn passkeys URL: https://docs.storno.ro/api-reference/auth/passkeys # Passkeys (WebAuthn) Implement passwordless authentication using WebAuthn passkeys. Users can register biometric or security key-based credentials for secure, phishing-resistant authentication. ## Overview Passkeys provide a modern, secure alternative to passwords using public-key cryptography. The system supports: - **Biometric authentication** (fingerprint, Face ID, Touch ID) - **Platform authenticators** (built into devices) - **Security keys** (YubiKey, etc.) - **Multiple passkeys** per user for different devices --- ## Registration Flow ### 1. Get Registration Options Generate a WebAuthn challenge for registering a new passkey. **Endpoint**: `POST /api/v1/passkey/register/options` **Authentication**: Required (Bearer token) #### Request ```bash curl -X POST https://api.storno.ro/api/v1/passkey/register/options \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \ -H "Content-Type: application/json" ``` ```js const response = await fetch('https://api.storno.ro/api/v1/passkey/register/options', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }); const options = await response.json(); ``` #### Response (200 OK) ```json { "rp": { "name": "Storno.ro", "id": "storno.ro" }, "user": { "id": "dXNlci1pZC0xMjM=", "name": "user@example.com", "displayName": "John Doe" }, "challenge": "cD_Q3UtKzJK8RpzGgqtbqA", "pubKeyCredParams": [ { "type": "public-key", "alg": -7 }, { "type": "public-key", "alg": -257 } ], "timeout": 60000, "attestation": "none", "authenticatorSelection": { "authenticatorAttachment": "platform", "requireResidentKey": true, "residentKey": "required", "userVerification": "required" } } ``` ### 2. Register Passkey Complete passkey registration with the credential response from the browser. **Endpoint**: `POST /api/v1/passkey/register` **Authentication**: Required (Bearer token) #### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `name` | string | Yes | Friendly name for the passkey (e.g., "iPhone 15", "YubiKey") | | `response` | object | Yes | WebAuthn credential creation response from browser | #### Request ```bash curl -X POST https://api.storno.ro/api/v1/passkey/register \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \ -H "Content-Type: application/json" \ -d '{ "name": "iPhone 15 Pro", "response": { "id": "AaFdkcE...", "rawId": "AaFdkcE...", "response": { "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiY0RfUTNVdEt6Sko4UnB6R2dxdGJxQSIsIm9yaWdpbiI6Imh0dHBzOi8vYXV0b2ZhY3R1cmEucm8ifQ", "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAGhXZHBAk..." }, "type": "public-key" } }' ``` ```js // Step 1: Get options const optionsResponse = await fetch('/api/v1/passkey/register/options', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }); const options = await optionsResponse.json(); // Step 2: Create credential with browser WebAuthn API const credential = await navigator.credentials.create({ publicKey: options, }); // Step 3: Register the passkey const registerResponse = await fetch('/api/v1/passkey/register', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'iPhone 15 Pro', response: credential, }), }); const result = await registerResponse.json(); ``` #### Response (201 Created) ```json { "id": "01HQZX1234ABCDEF5678WXYZ", "name": "iPhone 15 Pro", "createdAt": "2026-02-16T10:30:00Z", "lastUsedAt": null } ``` --- ## Authentication Flow ### 1. Get Login Options Generate a WebAuthn challenge for authentication. **Endpoint**: `POST /api/auth/passkey/login/options` **Authentication**: Not required (public endpoint) #### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `email` | string | No | User's email (optional, for filtering available passkeys) | #### Request ```bash curl -X POST https://api.storno.ro/api/auth/passkey/login/options \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com" }' ``` ```js const response = await fetch('https://api.storno.ro/api/auth/passkey/login/options', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'user@example.com', // Optional }), }); const options = await response.json(); ``` #### Response (200 OK) ```json { "challenge": "Z3vY8KpMqWr4TnxBftauDw", "timeout": 60000, "rpId": "storno.ro", "allowCredentials": [ { "type": "public-key", "id": "AaFdkcE..." } ], "userVerification": "required" } ``` ### 2. Login with Passkey Authenticate using a passkey credential. **Endpoint**: `POST /api/auth/passkey/login` **Authentication**: Not required (public endpoint) #### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `response` | object | Yes | WebAuthn assertion response from browser | #### Request ```bash curl -X POST https://api.storno.ro/api/auth/passkey/login \ -H "Content-Type: application/json" \ -d '{ "response": { "id": "AaFdkcE...", "rawId": "AaFdkcE...", "response": { "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiWjN2WThLcE1xV3I0VG54QmZ0YXVEdyIsIm9yaWdpbiI6Imh0dHBzOi8vYXV0b2ZhY3R1cmEucm8ifQ", "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAQ", "signature": "MEUCIQCvXW..." }, "type": "public-key" } }' ``` ```js // Step 1: Get options const optionsResponse = await fetch('/api/auth/passkey/login/options', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'user@example.com' }), }); const options = await optionsResponse.json(); // Step 2: Get credential with browser WebAuthn API const assertion = await navigator.credentials.get({ publicKey: options, }); // Step 3: Login with passkey const loginResponse = await fetch('/api/auth/passkey/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ response: assertion, }), }); const { token, refresh_token } = await loginResponse.json(); ``` #### Response (200 OK) ```json { "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "def50200a1b2c3d4e5f6..." } ``` --- ## Passkey Management ### List User Passkeys Get all passkeys registered for the current user. **Endpoint**: `GET /api/v1/me/passkeys` **Authentication**: Required (Bearer token) #### Request ```bash curl https://api.storno.ro/api/v1/me/passkeys \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." ``` ```js const response = await fetch('https://api.storno.ro/api/v1/me/passkeys', { headers: { 'Authorization': `Bearer ${accessToken}`, }, }); const passkeys = await response.json(); ``` #### Response (200 OK) ```json [ { "id": "01HQZX1234ABCDEF5678WXYZ", "name": "iPhone 15 Pro", "createdAt": "2026-02-15T14:22:00Z", "lastUsedAt": "2026-02-16T09:15:00Z" }, { "id": "01HQZY5678GHIJKL9012STUV", "name": "MacBook Pro", "createdAt": "2026-02-10T11:30:00Z", "lastUsedAt": "2026-02-16T08:00:00Z" }, { "id": "01HR0Z9012MNOPQR3456UVWX", "name": "YubiKey 5", "createdAt": "2026-02-01T16:45:00Z", "lastUsedAt": "2026-02-14T10:30:00Z" } ] ``` ### Delete Passkey Remove a registered passkey. **Endpoint**: `DELETE /api/v1/me/passkeys/{id}` **Authentication**: Required (Bearer token) #### Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | string | Yes | Passkey ID (path parameter) | #### Request ```bash curl -X DELETE https://api.storno.ro/api/v1/me/passkeys/01HQZX1234ABCDEF5678WXYZ \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." ``` ```js const response = await fetch( `https://api.storno.ro/api/v1/me/passkeys/${passkeyId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${accessToken}`, }, } ); ``` #### Response (204 No Content) No response body. --- ## Error Codes | Code | Description | |------|-------------| | `400` | Bad Request - Invalid request parameters or WebAuthn response | | `401` | Unauthorized - Invalid or expired authentication token | | `403` | Forbidden - User verification failed | | `404` | Not Found - Passkey not found or doesn't belong to user | | `409` | Conflict - Passkey already registered for this credential | | `422` | Unprocessable Entity - WebAuthn verification failed | ### Error Response Examples **Invalid WebAuthn Response (422)** ```json { "code": 422, "message": "WebAuthn verification failed: Invalid signature." } ``` **Passkey Already Registered (409)** ```json { "code": 409, "message": "This passkey is already registered." } ``` --- ## Browser Compatibility Passkeys are supported in modern browsers with WebAuthn API: - **Chrome/Edge**: 67+ - **Firefox**: 60+ - **Safari**: 13+ - **Mobile browsers**: iOS 14+, Android Chrome 70+ Check availability: ```js if (window.PublicKeyCredential) { // WebAuthn is supported } else { // Fallback to password authentication } ``` ## Best Practices 1. **Always offer fallback authentication** (password or OAuth) 2. **Use descriptive names** for passkeys (device/location) 3. **Allow multiple passkeys** per user for redundancy 4. **Handle errors gracefully** with clear user messaging 5. **Test across devices** to ensure compatibility 6. **Update lastUsedAt** tracking for security monitoring ## Security Notes - Passkeys use public-key cryptography, making them phishing-resistant - Private keys never leave the user's device - User verification (biometrics/PIN) is required by default - Passkeys cannot be reused across different domains - Server-side challenge validation prevents replay attacks --- ## Refresh Token > Refresh an expired JWT access token URL: https://docs.storno.ro/api-reference/auth/refresh # Refresh Token Exchange a refresh token for a new JWT access token and refresh token pair. Both tokens are rotated for enhanced security. ## Request ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `refresh_token` | string | Yes | Valid refresh token from previous login or refresh | ### Example Request ```bash curl -X POST https://api.storno.ro/api/auth/token/refresh \ -H "Content-Type: application/json" \ -d '{ "refresh_token": "def50200a1b2c3d4e5f6..." }' ``` ```js const response = await fetch('https://api.storno.ro/api/auth/token/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ refresh_token: 'def50200a1b2c3d4e5f6...', }), }); const { token, refresh_token } = await response.json(); ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->post('https://api.storno.ro/api/auth/token/refresh', [ 'json' => [ 'refresh_token' => 'def50200a1b2c3d4e5f6...', ], ]); $data = json_decode($response->getBody(), true); $token = $data['token']; $refreshToken = $data['refresh_token']; ``` ## Response ### Success Response (200 OK) ```json { "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "def50200b2c3d4e5f6a7..." } ``` | Field | Type | Description | |-------|------|-------------| | `token` | string | New JWT access token, valid for 1 hour | | `refresh_token` | string | New refresh token (the old one is invalidated) | ## Error Codes | Code | Description | |------|-------------| | `400` | Bad Request - Missing refresh token parameter | | `401` | Unauthorized - Invalid, expired, or revoked refresh token | | `403` | Forbidden - User account is inactive or deleted | ### Error Response Examples **Invalid Refresh Token (401)** ```json { "code": 401, "message": "Invalid refresh token." } ``` **Expired Refresh Token (401)** ```json { "code": 401, "message": "Refresh token has expired. Please login again." } ``` ## Usage Notes - **Token Rotation**: Both the access token and refresh token are replaced with new values on every refresh - **Old Token Invalidation**: The previous refresh token becomes invalid immediately after use - **Refresh Token Lifetime**: Refresh tokens are valid for 30 days from last use - **Security**: Store refresh tokens securely; do not expose them in URLs or logs - **Automatic Refresh**: Implement automatic token refresh before the access token expires to maintain uninterrupted sessions - **Single Use**: Each refresh token can only be used once; attempting to reuse it will fail ## Best Practices ```js // Example: Automatic token refresh let accessToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...'; let refreshToken = 'def50200a1b2c3d4e5f6...'; async function fetchWithAuth(url, options = {}) { const response = await fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${accessToken}`, }, }); // If token expired, refresh and retry if (response.status === 401) { const refreshResponse = await fetch('/api/auth/token/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: refreshToken }), }); if (refreshResponse.ok) { const tokens = await refreshResponse.json(); accessToken = tokens.token; refreshToken = tokens.refresh_token; // Retry original request return fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${accessToken}`, }, }); } else { // Refresh failed, redirect to login window.location.href = '/login'; } } return response; } ``` --- ## Regenerate Backup Codes > Generate a new set of backup codes, invalidating all previous codes URL: https://docs.storno.ro/api-reference/auth/mfa-backup-codes # Regenerate Backup Codes Generate a fresh set of 10 backup codes, immediately invalidating all previous codes. Requires password confirmation and TOTP to be enabled. ## Request ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `password` | string | Yes | User's current account password | ### Example Request ```bash curl -X POST https://api.storno.ro/api/v1/me/mfa/backup-codes/regenerate \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "password": "your-password" }' ``` ```js const response = await fetch('https://api.storno.ro/api/v1/me/mfa/backup-codes/regenerate', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ password: 'your-password' }), }); const { backupCodes } = await response.json(); ``` ## Response ### Success Response (200 OK) ```json { "backupCodes": [ "a3km-v7np", "h2bx-q9wt", "f4jy-m6cr", "d8ns-w3gp", "k5ht-b2xv", "p7mf-j4qs", "r9cw-n6yd", "t2gv-k8hb", "v6xp-f3mt", "w4qn-s7jc" ] } ``` All previous backup codes are immediately invalidated. Prompt the user to save the new codes securely. ## Error Codes | Code | Description | |------|-------------| | `400` | MFA is not enabled | | `401` | Unauthorized — missing or invalid JWT token | | `422` | Invalid password | --- ## Register > Create a new user account URL: https://docs.storno.ro/api-reference/auth/register # Register Create a new user account with email and password. A default organization is automatically created with the user as the owner. ## Request ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `email` | string | Yes | User's email address (must be unique and valid) | | `password` | string | Yes | Password (minimum 8 characters) | | `firstName` | string | No | User's first name | | `lastName` | string | No | User's last name | ### Example Request ```bash curl -X POST https://api.storno.ro/api/auth/register \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", "password": "SecurePass123!", "firstName": "John", "lastName": "Doe" }' ``` ```js const response = await fetch('https://api.storno.ro/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'user@example.com', password: 'SecurePass123!', firstName: 'John', lastName: 'Doe', }), }); const { token, refresh_token } = await response.json(); ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->post('https://api.storno.ro/api/auth/register', [ 'json' => [ 'email' => 'user@example.com', 'password' => 'SecurePass123!', 'firstName' => 'John', 'lastName' => 'Doe', ], ]); $data = json_decode($response->getBody(), true); $token = $data['token']; $refreshToken = $data['refresh_token']; ``` ## Response ### Success Response (201 Created) ```json { "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "def50200a1b2c3d4e5f6..." } ``` | Field | Type | Description | |-------|------|-------------| | `token` | string | JWT access token, valid for 1 hour | | `refresh_token` | string | Refresh token used to obtain new access tokens | ## Error Codes | Code | Description | |------|-------------| | `400` | Bad Request - Validation errors (invalid email, weak password, etc.) | | `409` | Conflict - Email address already registered | | `429` | Too Many Requests - Rate limit exceeded | ### Error Response Examples **Validation Error (400)** ```json { "code": 400, "message": "Validation failed", "errors": { "email": ["This value is not a valid email address."], "password": ["Password must be at least 8 characters long."] } } ``` **Email Already Exists (409)** ```json { "code": 409, "message": "An account with this email address already exists." } ``` ## Automatic Setup When you register a new account, the system automatically: 1. **Creates a User** with the provided email and hashed password 2. **Creates a Default Organization** named after the user's email 3. **Assigns Owner Role** via an organization membership 4. **Issues JWT Tokens** for immediate authenticated access ## Password Requirements Passwords must meet the following criteria: - Minimum 8 characters - At least one uppercase letter (recommended) - At least one lowercase letter (recommended) - At least one number (recommended) - Special characters are allowed and encouraged ## Email Confirmation Depending on configuration, the account may require email confirmation before full access is granted: - A confirmation email is sent to the provided address - The user may need to click the confirmation link before logging in - Check the [Confirm Email](/api-reference/auth/confirm-email) documentation for details ## Usage Notes - Email addresses are case-insensitive and stored in lowercase - The user is immediately authenticated after successful registration - Rate limiting applies: maximum 3 registration attempts per hour per IP address - The default organization can be renamed later via the organization settings - Users can create or join additional organizations after registration ## Next Steps After successful registration: 1. Store the tokens securely 2. Fetch user profile via [GET /api/v1/me](/api-reference/auth/me) 3. Set up company information for invoicing 4. Configure ANAF e-Factura integration --- ## Reset Password > Reset password using a reset token URL: https://docs.storno.ro/api-reference/auth/reset-password # Reset Password Reset a user's password using a valid reset token received via email. This completes the password recovery process started with [Forgot Password](/api-reference/auth/forgot-password). ## Request ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `token` | string | Yes | Password reset token from email link | | `password` | string | Yes | New password (minimum 8 characters) | ### Example Request ```bash curl -X POST https://api.storno.ro/api/auth/reset-password \ -H "Content-Type: application/json" \ -d '{ "token": "abc123xyz789def456ghi...", "password": "NewSecurePassword123!" }' ``` ```js // Extract token from URL const urlParams = new URLSearchParams(window.location.search); const token = urlParams.get('token'); const response = await fetch('https://api.storno.ro/api/auth/reset-password', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: token, password: 'NewSecurePassword123!', }), }); if (response.ok) { window.location.href = '/login?reset=success'; } ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->post('https://api.storno.ro/api/auth/reset-password', [ 'json' => [ 'token' => $_GET['token'], 'password' => $_POST['password'], ], ]); if ($response->getStatusCode() === 200) { header('Location: /login?reset=success'); exit; } ``` ## Response ### Success Response (200 OK) ```json { "message": "Password has been reset successfully. You can now log in with your new password." } ``` ## Error Codes | Code | Description | |------|-------------| | `400` | Bad Request - Invalid token format or password validation failed | | `401` | Unauthorized - Token is invalid, expired, or already used | | `422` | Unprocessable Entity - Password doesn't meet security requirements | ### Error Response Examples **Invalid or Expired Token (401)** ```json { "code": 401, "message": "Password reset token is invalid or has expired." } ``` **Token Already Used (401)** ```json { "code": 401, "message": "This password reset token has already been used." } ``` **Weak Password (422)** ```json { "code": 422, "message": "Validation failed", "errors": { "password": [ "Password must be at least 8 characters long.", "Password must contain at least one uppercase letter.", "Password must contain at least one number." ] } } ``` ## Password Requirements New passwords must meet the following criteria: - **Minimum length**: 8 characters - **Recommended**: Include uppercase letters, lowercase letters, numbers, and special characters - **Prohibited**: Cannot be a commonly used password (e.g., "password123", "12345678") - **Security**: Should not contain personal information (name, email, etc.) ## Security Behavior When a password is successfully reset: 1. **Password Updated** - New password hash is stored 2. **Token Invalidated** - Reset token is marked as used 3. **Previous Tokens Cleared** - All other reset tokens for this user are invalidated 4. **Sessions Revoked** - All existing user sessions are terminated 5. **Security Event Logged** - Password change is recorded for audit purposes 6. **Notification Sent** - User receives confirmation email (if email notifications are enabled) ## Complete User Flow ### 1. User Requests Reset User visits forgot password page and enters their email. ```js await fetch('/api/auth/forgot-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'user@example.com' }), }); ``` ### 2. User Receives Email System sends email with reset link: ``` https://storno.ro/reset-password?token=abc123xyz789def456ghi... ``` ### 3. User Clicks Link User is redirected to password reset form with token in URL. ### 4. User Submits New Password ```js const response = await fetch('/api/auth/reset-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: 'abc123xyz789def456ghi...', password: 'NewSecurePassword123!', }), }); ``` ### 5. User Logs In User is redirected to login page and can access their account with the new password. ## Implementation Example ### Frontend Reset Password Form ```html

Reset Your Password

``` ### Password Strength Indicator ```js function checkPasswordStrength(password) { let strength = 0; if (password.length >= 8) strength++; if (password.length >= 12) strength++; if (/[a-z]/.test(password)) strength++; if (/[A-Z]/.test(password)) strength++; if (/[0-9]/.test(password)) strength++; if (/[^a-zA-Z0-9]/.test(password)) strength++; const levels = ['Weak', 'Fair', 'Good', 'Strong', 'Very Strong']; return { score: strength, level: levels[Math.min(strength - 1, levels.length - 1)], percentage: (strength / 6) * 100, }; } ``` ## Token Expiration Reset tokens have a limited lifetime: - **Validity**: 1 hour from issuance - **Single-use**: Token is invalidated after successful use - **Automatic cleanup**: Expired tokens are periodically removed from database If a token expires: 1. User must request a new password reset 2. Previous token becomes permanently invalid 3. New token is generated and sent via email ## Troubleshooting ### Common Issues **"Token is invalid or has expired"** - Token has expired (older than 1 hour) - Token has already been used - Token was copied incorrectly from email - **Solution**: Request a new password reset **"Password doesn't meet requirements"** - Password is too short - Password lacks complexity - **Solution**: Use a stronger password with mixed characters **"Unable to reset password"** - Network connectivity issue - Server temporarily unavailable - **Solution**: Try again in a few minutes ### Best Practices 1. **Token handling**: - Extract token from URL query parameter - Validate token format before submission - Handle expired tokens gracefully 2. **Password validation**: - Show password requirements clearly - Provide real-time strength indicator - Require password confirmation 3. **User feedback**: - Show loading state during submission - Display clear error messages - Redirect to login after success 4. **Security**: - Never log or display tokens - Clear form on success - Don't reuse tokens ## Related Endpoints - [Forgot Password](/api-reference/auth/forgot-password) - Request a password reset email - [Login](/api-reference/auth/login) - Authenticate with new password - [Update Profile](/api-reference/auth/me) - Change password while logged in ## Security Notes - Tokens are cryptographically secure random strings (minimum 32 bytes) - Tokens are hashed before storage in database - Rate limiting prevents brute-force token guessing - All password reset attempts are logged with IP address and timestamp - Successful password reset triggers email notification to user - Old sessions are invalidated to force re-authentication --- ## Setup TOTP > Generate a TOTP secret and QR code for authenticator app setup URL: https://docs.storno.ro/api-reference/auth/mfa-totp-setup # Setup TOTP Generate a new TOTP secret for the authenticated user. Returns the secret, a QR code image (data URI), and the `otpauth://` URI for manual entry in authenticator apps. This does **not** enable MFA yet — the user must verify the first code via [Enable TOTP](/api-reference/auth/mfa-totp-enable) to activate it. ## Request No request body required. ### Example Request ```bash curl -X POST https://api.storno.ro/api/v1/me/mfa/totp/setup \ -H "Authorization: Bearer {token}" ``` ```js const response = await fetch('https://api.storno.ro/api/v1/me/mfa/totp/setup', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, }); const { secret, qrCode, otpauthUri } = await response.json(); ``` ## Response ### Success Response (200 OK) ```json { "secret": "JBSWY3DPEHPK3PXP", "qrCode": "data:image/png;base64,iVBORw0KGgo...", "otpauthUri": "otpauth://totp/Storno:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Storno" } ``` | Field | Type | Description | |-------|------|-------------| | `secret` | string | Base32-encoded TOTP secret for manual entry | | `qrCode` | string | PNG image as a data URI (300x300px) — render as an `` tag | | `otpauthUri` | string | `otpauth://` URI for deep-linking to authenticator apps | ## Error Codes | Code | Description | |------|-------------| | `401` | Unauthorized — missing or invalid JWT token | | `409` | Conflict — TOTP is already enabled for this user | ## Usage Notes - If an unverified secret already exists, calling this endpoint again overwrites it - The secret is not active until verified via [Enable TOTP](/api-reference/auth/mfa-totp-enable) - Display the QR code for the user to scan with Google Authenticator, Authy, or any TOTP-compatible app - Also display the `secret` string for users who prefer manual entry --- ## User Profile > Get and update current user profile information URL: https://docs.storno.ro/api-reference/auth/me # User Profile Manage the authenticated user's profile, including personal information, preferences, and account settings. --- ## Get Current User Retrieve the authenticated user's profile with organization and membership details. **Endpoint**: `GET /api/v1/me` **Authentication**: Required (Bearer token) ### Request ```bash curl https://api.storno.ro/api/v1/me \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." ``` ```js const response = await fetch('https://api.storno.ro/api/v1/me', { headers: { 'Authorization': `Bearer ${accessToken}`, }, }); const user = await response.json(); ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->get('https://api.storno.ro/api/v1/me', [ 'headers' => [ 'Authorization' => 'Bearer ' . $accessToken, ], ]); $user = json_decode($response->getBody(), true); ``` ### Response (200 OK) Returns flat JSON with user profile, organization, memberships, and subscription information. ```json { "id": "01HQZX1234ABCDEF5678WXYZ", "email": "user@example.com", "firstName": "John", "lastName": "Doe", "phone": "+40721234567", "timezone": "Europe/Bucharest", "respectQuietHours": true, "emailConfirmed": true, "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-02-16T09:15:00Z", "preferences": { "language": "ro", "theme": "light", "notifications": { "email": true, "push": false } }, "organization": { "id": "01HQZY5678GHIJKL9012STUV", "name": "My Company SRL", "slug": "my-company-srl", "createdAt": "2026-01-15T10:30:00Z" }, "memberships": [ { "id": "01HR0Z9012MNOPQR3456UVWX", "organizationId": "01HQZY5678GHIJKL9012STUV", "organizationName": "My Company SRL", "role": "owner", "joinedAt": "2026-01-15T10:30:00Z" } ], "subscription": { "plan": "pro", "status": "active", "expiresAt": "2027-01-15T10:30:00Z", "features": { "maxInvoices": 1000, "maxCompanies": 5, "apiAccess": true } } } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | User's unique identifier (ULID) | | `email` | string | User's email address | | `firstName` | string | User's first name | | `lastName` | string | User's last name | | `phone` | string | User's phone number | | `timezone` | string | User's timezone (IANA format) | | `respectQuietHours` | boolean | When true, push notifications are skipped between 22:00 and 08:00 in the user's timezone | | `emailConfirmed` | boolean | Whether email has been confirmed | | `createdAt` | string | Account creation timestamp (ISO 8601) | | `updatedAt` | string | Last profile update timestamp (ISO 8601) | | `preferences` | object | User preferences and settings | | `organization` | object | Current/default organization details | | `memberships` | array | All organization memberships | | `subscription` | object | Subscription plan and features | --- ## Update User Profile Update the authenticated user's profile information. **Endpoint**: `PATCH /api/v1/me` **Authentication**: Required (Bearer token) ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `firstName` | string | No | User's first name | | `lastName` | string | No | User's last name | | `phone` | string | No | Phone number (E.164 format recommended) | | `timezone` | string | No | Timezone (IANA format, e.g., "Europe/Bucharest") | | `respectQuietHours` | boolean | No | Mute push notifications during quiet hours (22:00–08:00 user-local) | | `preferences` | object | No | User preferences object | | `password` | string | No | New password (requires `currentPassword`) | | `currentPassword` | string | No | Current password (required when changing password) | ### Request ```bash curl -X PATCH https://api.storno.ro/api/v1/me \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \ -H "Content-Type: application/json" \ -d '{ "firstName": "John", "lastName": "Smith", "phone": "+40721234567", "timezone": "Europe/Bucharest", "preferences": { "language": "ro", "theme": "dark", "notifications": { "email": true, "push": true } } }' ``` ```js const response = await fetch('https://api.storno.ro/api/v1/me', { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ firstName: 'John', lastName: 'Smith', phone: '+40721234567', timezone: 'Europe/Bucharest', preferences: { language: 'ro', theme: 'dark', notifications: { email: true, push: true, }, }, }), }); const updatedUser = await response.json(); ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->patch('https://api.storno.ro/api/v1/me', [ 'headers' => [ 'Authorization' => 'Bearer ' . $accessToken, 'Content-Type' => 'application/json', ], 'json' => [ 'firstName' => 'John', 'lastName' => 'Smith', 'phone' => '+40721234567', 'timezone' => 'Europe/Bucharest', 'preferences' => [ 'language' => 'ro', 'theme' => 'dark', 'notifications' => [ 'email' => true, 'push' => true, ], ], ], ]); $updatedUser = json_decode($response->getBody(), true); ``` ### Change Password Example ```js const response = await fetch('https://api.storno.ro/api/v1/me', { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ currentPassword: 'OldPassword123!', password: 'NewSecurePassword456!', }), }); ``` ### Response (200 OK) Returns the updated user profile in the same format as `GET /api/v1/me`. ```json { "id": "01HQZX1234ABCDEF5678WXYZ", "email": "user@example.com", "firstName": "John", "lastName": "Smith", "phone": "+40721234567", "timezone": "Europe/Bucharest", "respectQuietHours": true, "emailConfirmed": true, "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-02-16T10:45:00Z", "preferences": { "language": "ro", "theme": "dark", "notifications": { "email": true, "push": true } }, "organization": { ... }, "memberships": [ ... ], "subscription": { ... } } ``` ### Error Codes | Code | Description | |------|-------------| | `400` | Bad Request - Invalid parameters or validation failed | | `401` | Unauthorized - Invalid or expired token | | `403` | Forbidden - Incorrect current password | | `422` | Unprocessable Entity - Invalid timezone or phone format | ### Error Response Examples **Validation Error (400)** ```json { "code": 400, "message": "Validation failed", "errors": { "phone": ["Phone number must be in E.164 format."], "timezone": ["Invalid timezone identifier."] } } ``` **Incorrect Current Password (403)** ```json { "code": 403, "message": "Current password is incorrect." } ``` --- ## Delete Account Permanently delete the user's account. This action soft-deletes the account and anonymizes personally identifiable information (PII). **Endpoint**: `DELETE /api/v1/me` **Authentication**: Required (Bearer token) ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `password` | string | Yes | Current password for confirmation | ### Request ```bash curl -X DELETE https://api.storno.ro/api/v1/me \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \ -H "Content-Type: application/json" \ -d '{ "password": "UserPassword123!" }' ``` ```js const response = await fetch('https://api.storno.ro/api/v1/me', { method: 'DELETE', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ password: 'UserPassword123!', }), }); if (response.ok) { // Account deleted, redirect to homepage window.location.href = '/'; } ``` ### Response (204 No Content) No response body. The account is successfully deleted and all tokens are invalidated. ### What Gets Deleted When you delete your account: 1. **User record** is soft-deleted (marked as deleted, not physically removed) 2. **Personal information** is anonymized: - Email replaced with `deleted-{timestamp}@example.com` - First and last name removed - Phone number removed - Profile picture removed 3. **Organization ownership** is transferred to the next admin (if any) 4. **Memberships** are removed from all organizations 5. **Sessions and tokens** are immediately revoked 6. **Invoices and financial data** are preserved for legal/tax compliance (anonymized) ### Error Codes | Code | Description | |------|-------------| | `400` | Bad Request - Missing password | | `401` | Unauthorized - Invalid or expired token | | `403` | Forbidden - Incorrect password | | `409` | Conflict - Cannot delete account with active subscriptions or pending invoices | ### Error Response Examples **Incorrect Password (403)** ```json { "code": 403, "message": "Password is incorrect. Account not deleted." } ``` **Active Subscription (409)** ```json { "code": 409, "message": "Cannot delete account with active subscription. Please cancel your subscription first." } ``` --- ## Usage Notes ### Preferences Object The `preferences` object is flexible and can contain any custom user settings: ```json { "language": "ro", "theme": "dark", "currency": "RON", "dateFormat": "d/m/Y", "notifications": { "email": true, "push": false, "sms": false, "invoiceReminders": true }, "dashboard": { "defaultView": "grid", "showStats": true } } ``` ### Timezone Support Valid timezone values follow the [IANA Time Zone Database](https://www.iana.org/time-zones): - `Europe/Bucharest` (Romania) - `Europe/London` (UK) - `America/New_York` (US Eastern) - `Asia/Tokyo` (Japan) - etc. ### Phone Number Format While any format is accepted, E.164 format is recommended for international compatibility: - **E.164 format**: `+40721234567` - **Alternative**: `0721234567` (national format) ### Security Best Practices 1. **Password changes** require the current password 2. **Account deletion** requires password confirmation 3. **Email changes** may require re-confirmation (check with support) 4. **Token invalidation** occurs on password change (user must re-login) 5. **Audit logging** tracks all profile changes for security ### Rate Limiting - Profile updates: 10 requests per minute - Account deletion: 3 attempts per hour (failed password attempts) --- ## Verify MFA Challenge > Complete a multi-factor authentication challenge with a TOTP code or backup code URL: https://docs.storno.ro/api-reference/auth/mfa-verify # Verify MFA Challenge Complete a pending MFA challenge by submitting a TOTP code from an authenticator app or a single-use backup code. Returns JWT tokens on success. ## Request ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `mfaToken` | string | Yes | The 64-character challenge token received from the login response | | `code` | string | Yes | 6-digit TOTP code or 8-character backup code (format: `xxxx-xxxx`) | | `type` | string | No | `totp` (default) or `backup` | ### Example Request ```bash curl -X POST https://api.storno.ro/api/auth/mfa/verify \ -H "Content-Type: application/json" \ -d '{ "mfaToken": "a1b2c3d4e5f6...", "code": "123456", "type": "totp" }' ``` ```js const response = await fetch('https://api.storno.ro/api/auth/mfa/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mfaToken: 'a1b2c3d4e5f6...', code: '123456', type: 'totp', }), }); const { token, refresh_token } = await response.json(); ``` ```php $response = $client->post('https://api.storno.ro/api/auth/mfa/verify', [ 'json' => [ 'mfaToken' => 'a1b2c3d4e5f6...', 'code' => '123456', 'type' => 'totp', ], ]); $data = json_decode($response->getBody(), true); ``` ## Response ### Success Response (200 OK) ```json { "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "def50200a1b2c3d4e5f6..." } ``` | Field | Type | Description | |-------|------|-------------| | `token` | string | JWT access token | | `refresh_token` | string | Refresh token for obtaining new access tokens | ## Error Codes | Code | Description | |------|-------------| | `400` | Missing `mfaToken` or `code` | | `401` | Invalid or expired challenge token, or invalid code | | `429` | Too many failed attempts (max 5 per challenge) | ### Error Response Examples **Invalid Code (401)** ```json { "error": "Invalid code." } ``` **Expired Challenge (401)** ```json { "error": "Invalid or expired MFA challenge." } ``` **Rate Limited (429)** ```json { "error": "Too many failed attempts. Please log in again." } ``` ## Usage Notes - Challenge tokens expire after **5 minutes** - Each challenge allows a maximum of **5 attempts** before being invalidated - Challenge tokens are single-use — they are deleted after successful verification - Backup codes are also single-use and cannot be reused - Backup codes accept input with or without dashes and are case-insensitive --- ## Create bank account > Add a new bank account to the company. URL: https://docs.storno.ro/api-reference/bank-accounts/create # Create bank account Creates a new bank account for the authenticated company. The IBAN must be unique within the company. ```http POST /api/v1/bank-accounts ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `type` | string | No | Account type: `bank` (default) or `cash`. A company can have at most one `cash` account; it represents the physical till that backs the POS / cash-register reports. | | `iban` | string | Conditional | International Bank Account Number. Required for `bank` accounts; ignored / nullable for `cash` accounts. | | `bankName` | string | No | Name of the bank (typically only set for `bank` accounts). | | `currency` | string | No | Currency code (ISO 4217, default: "RON") | | `isDefault` | boolean | No | Set as default account for this currency (default: false) | | `openingBalance` | number | No | Starting balance recorded for the account. Required to enable cash-register reporting on `cash` accounts; once set, locks and can only be modified via forward-going cash movements. Must be ≥ 0. | | `openingBalanceDate` | string | No | Date the opening balance was taken (YYYY-MM-DD). Required when `openingBalance` is provided. | ## Response Returns the created bank account object with a `201 Created` status. ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/bank-accounts' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "iban": "RO49INGB0000999900000017", "bankName": "ING Bank", "currency": "RON", "isDefault": false }' ``` ### Cash account example ```bash curl -X POST 'https://api.storno.ro/api/v1/bank-accounts' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "type": "cash", "currency": "RON", "openingBalance": 250.00, "openingBalanceDate": "2026-04-25" }' ``` ## Example Response ```json { "uuid": "bank-account-uuid-3", "iban": "RO49INGB0000999900000017", "bankName": "ING Bank", "currency": "RON", "isDefault": false, "createdAt": "2026-02-16T15:30:00Z", "updatedAt": "2026-02-16T15:30:00Z" } ``` ## Default Account Behavior - If `isDefault` is `true`, any existing default account for that currency will be set to non-default - Each currency can only have one default account - If this is the first account for a currency, it automatically becomes the default regardless of the `isDefault` value ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - Missing `iban` field - Invalid IBAN format - IBAN already exists for this company - Invalid currency code ## Related Endpoints - [List bank accounts](/api-reference/bank-accounts/list) - [Update bank account](/api-reference/bank-accounts/update) - [Delete bank account](/api-reference/bank-accounts/delete) --- ## Delete bank account > Permanently delete a bank account. URL: https://docs.storno.ro/api-reference/bank-accounts/delete # Delete bank account Permanently deletes a bank account from the authenticated company. ```http DELETE /api/v1/bank-accounts/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the bank account | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns a `204 No Content` status on successful deletion with no response body. ## Example Request ```bash curl -X DELETE 'https://api.storno.ro/api/v1/bank-accounts/bank-account-uuid-3' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```http HTTP/1.1 204 No Content ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Bank account not found or doesn't belong to company | | 409 | conflict | Cannot delete the last bank account or account with active invoices | ## Important Notes - This is a permanent delete operation - data cannot be recovered - You cannot delete the last bank account for a company - If the account is marked as default, another account must be set as default first - Existing invoices that reference this bank account will retain the IBAN in their stored data - Consider setting `isDefault: false` on another account before deleting a default account ## Related Endpoints - [List bank accounts](/api-reference/bank-accounts/list) - [Create bank account](/api-reference/bank-accounts/create) - [Update bank account](/api-reference/bank-accounts/update) --- ## List bank accounts > Retrieve all bank accounts for the authenticated company. URL: https://docs.storno.ro/api-reference/bank-accounts/list # List bank accounts Retrieves all bank accounts configured for the authenticated company. ```http GET /api/v1/bank-accounts ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns an array of bank account objects. ### Bank Account Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `iban` | string | International Bank Account Number | | `bankName` | string \| null | Name of the bank | | `currency` | string | Currency code (ISO 4217, default: "RON") | | `isDefault` | boolean | Whether this is the default account | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/bank-accounts' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json [ { "uuid": "bank-account-uuid-1", "iban": "RO49AAAA1B31007593840000", "bankName": "BCR", "currency": "RON", "isDefault": true, "createdAt": "2025-06-01T10:00:00Z", "updatedAt": "2025-06-01T10:00:00Z" }, { "uuid": "bank-account-uuid-2", "iban": "RO49BTRL01101205N50289XX", "bankName": "Banca Transilvania", "currency": "EUR", "isDefault": false, "createdAt": "2025-07-15T14:30:00Z", "updatedAt": "2025-07-15T14:30:00Z" } ] ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | ## Important Notes - Each company must have at least one bank account - Only one bank account per currency can be marked as default - The default bank account is automatically used on new invoices - Bank accounts are displayed on invoices and used for payment instructions ## Related Endpoints - [Create bank account](/api-reference/bank-accounts/create) - [Update bank account](/api-reference/bank-accounts/update) - [Delete bank account](/api-reference/bank-accounts/delete) --- ## Update bank account > Update an existing bank account. URL: https://docs.storno.ro/api-reference/bank-accounts/update # Update bank account Updates an existing bank account for the authenticated company. ```http PATCH /api/v1/bank-accounts/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the bank account | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters All parameters are optional, but at least one must be provided. | Parameter | Type | Description | |-----------|------|-------------| | `type` | string | `bank` or `cash`. Switching type after creation is allowed but should be avoided once movements exist. | | `iban` | string \| null | International Bank Account Number. Nullable for `cash` accounts. | | `bankName` | string \| null | Name of the bank | | `currency` | string | Currency code (ISO 4217) | | `isDefault` | boolean | Set as default account for this currency | | `openingBalance` | number | Initial cash-on-hand. Once a value > 0 has been persisted it locks — further changes are rejected; correct via cash movements instead. | | `openingBalanceDate` | string | Date the opening balance was taken (YYYY-MM-DD). Required when `openingBalance` is being set for the first time. | ## Response Returns the updated bank account object. ## Example Request ```bash curl -X PATCH 'https://api.storno.ro/api/v1/bank-accounts/bank-account-uuid-1' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "bankName": "Banca Comercială Română", "isDefault": true }' ``` ## Example Response ```json { "uuid": "bank-account-uuid-1", "iban": "RO49AAAA1B31007593840000", "bankName": "Banca Comercială Română", "currency": "RON", "isDefault": true, "createdAt": "2025-06-01T10:00:00Z", "updatedAt": "2026-02-16T15:45:00Z" } ``` ## Default Account Behavior - If `isDefault` is set to `true`, any existing default account for that currency will be set to non-default - Each currency can only have one default account - Setting `isDefault` to `false` on a default account requires another account to be set as default first ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Bank account not found or doesn't belong to company | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - No fields provided for update - Invalid IBAN format - IBAN already exists for another account in this company - Invalid currency code - Cannot unset default without setting another account as default ## Related Endpoints - [List bank accounts](/api-reference/bank-accounts/list) - [Create bank account](/api-reference/bank-accounts/create) - [Delete bank account](/api-reference/bank-accounts/delete) --- ## Cash register balance > Live snapshot of the till — opening balance, cash in/out since opening, and current balance. URL: https://docs.storno.ro/api-reference/cash-register/balance # Cash register balance Returns a live snapshot of the company's cash drawer. The till is backed by a `BankAccount` of `type=cash`; the response folds together cash receipts, cash-collected payments on invoices, and any manual cash movements (deposits, withdrawals, miscellaneous). ```http GET /api/v1/cash-register/balance ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token | | `X-Company` | string | Yes | Company UUID | ## Response ### Cash account not configured `200 OK`. Returned when no cash-type bank account exists for the company. ```json { "configured": false } ``` ### Cash account exists but opening balance not set `200 OK`. The account exists but is missing `openingBalance` / `openingBalanceDate`, so calculations can't run yet. ```json { "configured": false, "cashAccountId": "9b21e0c0-1d1c-4f7d-8e8b-1d1c1f7d8e8b", "currency": "RON" } ``` ### Configured `200 OK`. All amounts are returned as strings to preserve decimal precision. ```json { "configured": true, "cashAccountId": "9b21e0c0-1d1c-4f7d-8e8b-1d1c1f7d8e8b", "currency": "RON", "openingBalance": "250.00", "openingBalanceDate": "2026-04-25", "cashReceipts": "1240.00", "cashPayments": "0.00", "manualNet": "-100.00", "currentBalance": "1390.00" } ``` | Field | Description | |-------|-------------| | `cashReceipts` | Sum of cash payments on issued/non-cancelled receipts since `openingBalanceDate`. Uses the `cashPayment` column when set (split tendering); falls back to the receipt total when `paymentMethod = 'cash'` and `cashPayment` is null. | | `cashPayments` | Sum of cash-paid payments recorded against invoices since `openingBalanceDate`. | | `manualNet` | Net effect of manual cash movements in the same period (deposits/withdrawals/miscellaneous), positive = net cash into the till. | | `currentBalance` | `openingBalance + cashReceipts − cashPayments + manualNet`. | ## Permissions Requires `report.view`. ## Errors | Status | Description | |--------|-------------| | 401 | Missing or invalid bearer token | | 403 | Permission denied or missing `X-Company` | | 404 | Company not found | --- ## Cash register ledger > Daily ledger of every cash entry (receipts, payments, manual movements) with running balance. URL: https://docs.storno.ro/api-reference/cash-register/ledger # Cash register ledger Returns one bucket per day in the requested range, each containing the chronological list of cash entries (receipts, payments, manual movements) along with the opening balance, totals, and closing balance for that day. Useful for end-of-day cash reconciliation. ```http GET /api/v1/cash-register/ledger?from=2026-04-25&to=2026-04-30 ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token | | `X-Company` | string | Yes | Company UUID | ## Query parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `from` | string | No | Start date (YYYY-MM-DD). Defaults to the cash account's `openingBalanceDate`. Clamped to ≥ opening date. | | `to` | string | No | End date (YYYY-MM-DD). Defaults to today. Must be ≥ `from`. | The maximum span between `from` and `to` is 366 days; larger ranges return `400 Bad Request`. ## Response ### Not configured ```json { "configured": false, "days": [] } ``` ### Configured ```json { "configured": true, "currency": "RON", "openingBalanceDate": "2026-04-25", "from": "2026-04-25", "to": "2026-04-26", "days": [ { "date": "2026-04-25", "opening": "250.00", "totalIn": "1240.00", "totalOut": "0.00", "closing": "1490.00", "entries": [ { "kind": "receipt", "documentNumber": "BON-2026-042", "documentType": "chitanta", "description": "Acme SRL", "in": "125.50", "out": "0.00", "balanceAfter": "375.50", "sourceId": "a1b2c3..." } ] } ] } ``` ### Entry shape | Field | Description | |-------|-------------| | `kind` | `receipt` (POS sale), `payment` (cash collection on an invoice), or `movement` (manual deposit/withdrawal/other) | | `documentNumber` | Receipt number, invoice number, or movement document number | | `documentType` | `chitanta`, `plata`, `depunere`, `ridicare`, `altele` | | `description` | Customer name (receipt), counterparty (payment), or description (movement) | | `in` / `out` | Amount in/out of the till for this entry (mutually exclusive — the other is `"0.00"`) | | `balanceAfter` | Running till balance after this entry | | `sourceId` | UUID of the underlying receipt / payment / movement | | `movementId` | (movements only) Same as `sourceId`, kept for backwards-compat | ## Permissions Requires `report.view`. ## Errors | Status | Description | |--------|-------------| | 400 | Invalid date format, `to` before `from`, or range > 366 days | | 401 | Missing or invalid bearer token | | 403 | Permission denied or missing `X-Company` | | 404 | Company not found | --- ## Create cash movement > Record a manual cash movement (deposit, withdrawal, or miscellaneous entry). URL: https://docs.storno.ro/api-reference/cash-register/movements-create # Create cash movement Records a manual entry against the company's cash drawer. Used for end-of-day deposits, withdrawals to the bank or safe, and miscellaneous events that aren't captured by receipts or invoice payments. ```http POST /api/v1/cash-register/movements ``` A cash account (`BankAccount` with `type=cash`) must be configured for the company. The movement currency is auto-set to the cash account's currency — clients should not send it. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token | | `X-Company` | string | Yes | Company UUID | | `Content-Type` | string | Yes | `application/json` | ## Body parameters | Field | Type | Required | Description | |-------|------|----------|-------------| | `kind` | string | Yes | `deposit` (cash leaves the till for the bank/safe), `withdrawal` (cash put into the till), or `other` (miscellaneous; `direction` must be supplied) | | `direction` | string | Conditional | `in` or `out`. Required only when `kind=other`; ignored otherwise (deposits are auto `out`, withdrawals auto `in`). | | `amount` | number | Yes | Positive amount, ≥ 0.01 | | `movementDate` | string | Yes | YYYY-MM-DD. Cannot be earlier than the cash account's `openingBalanceDate`. | | `description` | string | No | Free-form note shown in the ledger | | `documentNumber` | string | No | Reference number — e.g. bank slip number, expense receipt number | ## Direction semantics | Kind | Direction | Effect on till | |------|-----------|----------------| | `deposit` | `out` (auto) | Till balance decreases — cash physically left the drawer | | `withdrawal` | `in` (auto) | Till balance increases — cash was added to the drawer | | `other` | `in` or `out` (required) | Caller decides; useful for petty-cash purchases (`out`) or unexpected cash income (`in`) | ## Example ```bash curl -X POST 'https://api.storno.ro/api/v1/cash-register/movements' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "kind": "deposit", "amount": 200.00, "movementDate": "2026-04-26", "description": "End-of-day deposit at BCR", "documentNumber": "DEP-042" }' ``` ## Response `201 Created`. ```json { "id": "f1d3a4...", "movementDate": "2026-04-26", "kind": "deposit", "direction": "out", "amount": "200.00", "currency": "RON", "description": "End-of-day deposit at BCR", "documentNumber": "DEP-042", "createdAt": "2026-04-26T18:14:33+03:00" } ``` ## Permissions Requires `settings.manage`. ## Errors | Status | Description | |--------|-------------| | 400 | Invalid `kind`, invalid `direction`, missing `amount`/`movementDate`, or `movementDate` before opening date | | 401 | Unauthenticated | | 403 | Permission denied | | 409 | No cash account configured for the company | --- ## Delete cash movement > Remove a manual cash movement from the ledger. URL: https://docs.storno.ro/api-reference/cash-register/movements-delete # Delete cash movement Permanently deletes a manual cash movement. Receipts and invoice payments cannot be deleted via this endpoint — use their respective resources. ```http DELETE /api/v1/cash-register/movements/{uuid} ``` ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Movement UUID | ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token | | `X-Company` | string | Yes | Company UUID | ## Response `200 OK`. ```json { "message": "Movement deleted." } ``` ## Permissions Requires `settings.manage`. ## Errors | Status | Description | |--------|-------------| | 401 | Unauthenticated | | 403 | Permission denied | | 404 | Movement not found in this company | --- ## List cash movements > List manual cash movements (deposits, withdrawals, miscellaneous) in a date range. URL: https://docs.storno.ro/api-reference/cash-register/movements-list # List cash movements Returns the company's manual cash movements — deposits into the till, withdrawals to the safe / bank, or miscellaneous entries (e.g. covering a small expense from petty cash). Receipts and invoice payments are NOT returned here; see [`/cash-register/ledger`](/api-reference/cash-register/ledger) for the full picture. ```http GET /api/v1/cash-register/movements?from=2026-04-01&to=2026-04-30 ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token | | `X-Company` | string | Yes | Company UUID | ## Query parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `from` | string | No | Start date (YYYY-MM-DD). Defaults to the cash account's opening date or 30 days ago. | | `to` | string | No | End date (YYYY-MM-DD). Defaults to today. | ## Response ```json { "data": [ { "id": "f1d3a4...", "movementDate": "2026-04-26", "kind": "deposit", "direction": "out", "amount": "200.00", "currency": "RON", "description": "End-of-day deposit at BCR", "documentNumber": "DEP-042", "createdAt": "2026-04-26T18:14:33+03:00" } ] } ``` If no cash account is configured, an empty `data` array is returned. ## Permissions Requires `report.view`. --- ## Update cash movement > Edit a previously recorded manual cash movement. URL: https://docs.storno.ro/api-reference/cash-register/movements-update # Update cash movement Updates a manual cash movement. All fields from the create endpoint are accepted as partial — omitted fields keep their current value. The currency cannot be changed. ```http PATCH /api/v1/cash-register/movements/{uuid} ``` ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Movement UUID | ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token | | `X-Company` | string | Yes | Company UUID | | `Content-Type` | string | Yes | `application/json` | ## Body parameters Same shape as [Create cash movement](/api-reference/cash-register/movements-create), all optional. Special rules: - Changing `kind` between `deposit`/`withdrawal` re-applies the auto-direction. - `direction` is only honoured while `kind=other`. - `movementDate` is still bound to the cash account's `openingBalanceDate`. ## Response `200 OK`. Same shape as create. ## Permissions Requires `settings.manage`. ## Errors | Status | Description | |--------|-------------| | 400 | Invalid field value or backdated `movementDate` | | 401 | Unauthenticated | | 403 | Permission denied | | 404 | Movement not found in this company | --- ## Centrifugo Connection Token > Generate JWT token for Centrifugo WebSocket connection URL: https://docs.storno.ro/api-reference/centrifugo/connection-token # Centrifugo Connection Token Generate a JWT token for establishing a Centrifugo WebSocket connection. --- ## Generate Connection Token ```http POST /api/v1/centrifugo/connection-token ``` Generate a JWT token for authenticating the WebSocket connection to Centrifugo real-time server. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response ```json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | token | string | JWT token for Centrifugo connection | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | ### Usage Use this token to connect to the Centrifugo WebSocket server: ```javascript import { Centrifuge } from 'centrifuge'; // Get connection token const { token } = await $fetch('/api/v1/centrifugo/connection-token', { method: 'POST', headers: { Authorization: `Bearer ${authToken}` } }); // Connect to Centrifugo const centrifuge = new Centrifuge('wss://realtime.storno.ro/connection/websocket', { token: token }); centrifuge.connect(); ``` ### Token Claims The connection token includes: - User ID - Expiration time (typically 1 hour) - Connection metadata ### Notes - Connection tokens expire after 1 hour - Request a new token when the connection expires - Each user has their own connection token - Token is used only for initial connection authentication - Channel subscriptions require separate subscription tokens --- ## Centrifugo Subscription Token > Generate JWT token for subscribing to Centrifugo channels URL: https://docs.storno.ro/api-reference/centrifugo/subscription-token # Centrifugo Subscription Token Generate a JWT token for subscribing to specific Centrifugo channels. --- ## Generate Subscription Token ```http POST /api/v1/centrifugo/subscription-token ``` Generate a JWT token for subscribing to a specific Centrifugo channel. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | | Content-Type | string | Yes | Must be `application/json` | ### Request Body ```json { "channel": "user:123e4567-e89b-12d3-a456-426614174000" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | channel | string | Yes | Channel name to subscribe to | ### Response ```json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | token | string | JWT token for channel subscription | ### Channel Types | Channel Pattern | Description | Access | |----------------|-------------|--------| | `user:{uuid}` | User personal channel | Own user only | | `company:{uuid}` | Company channel | Members with company access | | `organization:{uuid}` | Organization channel | Organization members | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - no access to requested channel | | 422 | Validation error - invalid channel name | ### Usage Use this token to subscribe to channels: ```javascript // After connecting to Centrifugo const { token } = await $fetch('/api/v1/centrifugo/subscription-token', { method: 'POST', headers: { Authorization: `Bearer ${authToken}` }, body: { channel: 'user:123e4567-e89b-12d3-a456-426614174000' } }); const subscription = centrifuge.newSubscription('user:123e4567-e89b-12d3-a456-426614174000', { token: token }); subscription.on('publication', (ctx) => { console.log('Received:', ctx.data); }); subscription.subscribe(); ``` ### Real-time Events Events published to channels: #### User Channel Events - `notification.created` - New notification - `sync.completed` - Sync finished - `invoice.updated` - Invoice status changed #### Company Channel Events - `invoice.created` - New invoice - `invoice.updated` - Invoice updated - `member.joined` - New member added #### Organization Channel Events - `member.invited` - New invitation sent - `company.added` - New company added - `settings.changed` - Settings updated ### Notes - Subscription tokens expire after 1 hour - Request new tokens before expiration - Users can only subscribe to channels they have access to - Access control is enforced by the backend - Each channel requires a separate subscription token --- ## ANAF lookup > Look up a Romanian company by CUI in the ANAF registry URL: https://docs.storno.ro/api-reference/clients/anaf-lookup # ANAF lookup Looks up a Romanian company by CUI in the ANAF (National Agency for Fiscal Administration) registry without creating a client. Returns company details that can be used to pre-fill a client creation form. ```http GET /api/v1/clients/anaf-lookup ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `cui` | string | Yes | CUI/CIF to look up (with or without `RO` prefix) | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/clients/anaf-lookup?cui=12345678' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Response Returns the company details from ANAF. ```json { "data": { "cui": "12345678", "name": "EXEMPLU SRL", "address": "Str. Exemplu, Nr. 10", "city": "Bucuresti Sectorul 1", "county": "Bucuresti", "postalCode": "010101", "phone": "+40211234567", "registrationNumber": "J40/1234/2020", "isVatPayer": true, "vatCode": "RO12345678" } } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `cui` | string | CUI number (without RO prefix) | | `name` | string | Registered company name | | `address` | string | Registered street address | | `city` | string | City (Bucharest sectors are normalized to UBL format) | | `county` | string | County (Bucharest sectors are normalized) | | `postalCode` | string \| null | Postal code | | `phone` | string \| null | Phone number | | `registrationNumber` | string \| null | Trade register number | | `isVatPayer` | boolean | Whether the company is registered for VAT | | `vatCode` | string \| null | Full VAT code with RO prefix (if VAT payer) | ## Errors | Status Code | Description | |-------------|-------------| | 401 | Invalid or missing authentication token | | 404 | CUI not found in ANAF | | 422 | CUI is required | | 503 | ANAF lookup service unavailable | ## Related Endpoints - [Create client](/api-reference/clients/create) - [Create from registry](/api-reference/clients/from-registry) --- ## Bulk delete clients > Soft-delete multiple clients at once URL: https://docs.storno.ro/api-reference/clients/bulk-delete # Bulk delete clients Soft-deletes multiple clients in a single request. Each client is processed independently — if some deletions fail (e.g., permission denied), others will still succeed. ```http POST /api/v1/clients/bulk-delete ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Request body | Name | Type | Required | Description | |------|------|----------|-------------| | `ids` | string[] | Yes | Array of client UUIDs to delete (1–100 items) | ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/clients/bulk-delete' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "ids": [ "b2c3d4e5-f6a7-8901-bcde-f12345678901", "c3d4e5f6-a7b8-9012-cdef-123456789012" ] }' ``` ## Response Returns a summary of the operation. ```json { "deleted": 2, "errors": [] } ``` ### Partial failure If some clients cannot be deleted, the response includes both the count of successful deletions and an array of errors: ```json { "deleted": 1, "errors": [ { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "error": "Access Denied." } ] } ``` ## Errors | Status Code | Description | |-------------|-------------| | 400 | Invalid request (empty array, more than 100 IDs) | | 401 | Invalid or missing authentication token | ## Related Endpoints - [Delete client](/api-reference/clients/delete) - [List clients](/api-reference/clients/list) --- ## Create client > Create a new client manually URL: https://docs.storno.ro/api-reference/clients/create # Create client Creates a new client for the specified company. If a client with the same CUI already exists, returns the existing client instead of creating a duplicate. For foreign EU clients with a VAT code, the system automatically validates the VAT number against the EU VIES system and stores the result. ```http POST /api/v1/clients ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Request body | Name | Type | Required | Description | |------|------|----------|-------------| | `name` | string | Yes | Company name or individual full name | | `type` | string | No | Client type: `company` or `individual` (default: `company`) | | `cui` | string | No | CUI (tax identification number) for companies | | `cnp` | string | No | CNP (personal identification number) for individuals | | `vatCode` | string | No | Full VAT code with country prefix (e.g., `RO12345678`, `DE812526315`) | | `isVatPayer` | boolean | No | Whether the client is registered for VAT (default: false) | | `registrationNumber` | string | Conditional | Company registration number — required for Romanian companies | | `address` | string | Yes | Street address | | `city` | string | Yes | City name | | `county` | string | Conditional | County — required for Romanian clients | | `country` | string | No | ISO 3166-1 alpha-2 country code (default: `RO`) | | `postalCode` | string | No | Postal/ZIP code | | `email` | string | No | Email address | | `phone` | string | No | Phone number | | `bankName` | string | No | Bank name | | `bankAccount` | string | No | Bank account number (IBAN) | | `defaultPaymentTermDays` | integer | No | Default payment term in days | | `contactPerson` | string | No | Contact person name | | `notes` | string | No | Internal notes | | `idNumber` | string | No | Client identification number (personal ID, passport, etc.) | | `currency` | string | No | Preferred currency for this client (ISO 4217: EUR, USD, RON, etc.) | ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/clients' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "name": "Example GmbH", "type": "company", "vatCode": "DE812526315", "isVatPayer": true, "address": "Musterstrasse 1", "city": "Berlin", "country": "DE", "postalCode": "10115", "email": "billing@example.de" }' ``` ## Response Returns the created client object with status `201 Created`. ```json { "client": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "type": "company", "name": "Example GmbH", "cui": null, "cnp": null, "vatCode": "DE812526315", "isVatPayer": true, "address": "Musterstrasse 1", "city": "Berlin", "county": null, "country": "DE", "postalCode": "10115", "email": "billing@example.de", "viesValid": true, "viesValidatedAt": "2026-03-02T12:00:00+02:00", "viesName": "EXAMPLE GMBH", "source": "manual", "createdAt": "2026-03-02T12:00:00+02:00" } } ``` ### Duplicate CUI handling If a client with the same CUI already exists for this company, the endpoint returns the existing client with `200 OK` instead of creating a duplicate: ```json { "client": { ... }, "existing": true } ``` ### VIES auto-validation When a foreign EU client is created with a VAT code, the system automatically validates it against the EU VIES system. The result is stored in: - `viesValid` — `true` if valid, `false` if invalid, `null` if not applicable - `viesValidatedAt` — timestamp of validation - `viesName` — company name as registered in VIES This only applies to EU member state countries (AT, BE, BG, CY, CZ, DE, DK, EE, EL, ES, FI, FR, HR, HU, IE, IT, LT, LU, LV, MT, NL, PL, PT, SE, SI, SK). Romanian and non-EU clients are not validated. ## Errors | Status Code | Description | |-------------|-------------| | 401 | Invalid or missing authentication token | | 403 | Permission denied | | 404 | Company not found | | 422 | Validation error (missing required fields) | ## Related Endpoints - [List clients](/api-reference/clients/list) - [Get client](/api-reference/clients/get) - [Create from registry](/api-reference/clients/from-registry) - [ANAF lookup](/api-reference/clients/anaf-lookup) --- ## Create from registry > Create a client from the ONRC registry with ANAF validation URL: https://docs.storno.ro/api-reference/clients/from-registry # Create from registry Creates a client by looking up a CUI in the ANAF registry and auto-filling all available details. If a client with the same CUI already exists, returns the existing client instead of creating a duplicate. This is the recommended way to add Romanian company clients — it ensures all fiscal data (VAT status, registration number, address) is accurate and up to date from official sources. ```http POST /api/v1/clients/from-registry ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Request body | Name | Type | Required | Description | |------|------|----------|-------------| | `cui` | string | Yes | CUI/CIF to look up (with or without `RO` prefix) | | `name` | string | No | Fallback name if ANAF lookup fails | ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/clients/from-registry' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "cui": "12345678" }' ``` ## Response Returns the created client with status `201 Created`, along with whether ANAF validation succeeded. ```json { "client": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "type": "company", "name": "EXEMPLU SRL", "cui": "12345678", "vatCode": "RO12345678", "isVatPayer": true, "registrationNumber": "J40/1234/2020", "address": "Str. Exemplu, Nr. 10", "city": "Bucuresti Sectorul 1", "county": "Bucuresti", "country": "RO", "postalCode": "010101", "phone": "+40211234567", "source": "manual", "createdAt": "2026-03-02T12:00:00+02:00" }, "anafValidated": true } ``` ### Duplicate CUI handling If a client with the same CUI already exists, returns the existing client with `200 OK`: ```json { "client": { ... }, "existing": true } ``` ### ANAF lookup failure If the ANAF service is unavailable, the client is still created with the CUI and the fallback `name` (or `"CUI 12345678"` if no name provided). The `anafValidated` field will be `false`. ## Errors | Status Code | Description | |-------------|-------------| | 401 | Invalid or missing authentication token | | 403 | Permission denied | | 404 | Company not found | | 422 | CUI is required | ## Related Endpoints - [Create client](/api-reference/clients/create) - [ANAF lookup](/api-reference/clients/anaf-lookup) - [List clients](/api-reference/clients/list) --- ## Delete client > Soft-delete a client URL: https://docs.storno.ro/api-reference/clients/delete # Delete client Soft-deletes a client. The client is marked as deleted but remains in the database. Soft-deleted clients are excluded from list queries and cannot be used on new invoices. ```http DELETE /api/v1/clients/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the client | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Example Request ```bash curl -X DELETE 'https://api.storno.ro/api/v1/clients/b2c3d4e5-f6a7-8901-bcde-f12345678901' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Response Returns `204 No Content` on success. ## Errors | Status Code | Description | |-------------|-------------| | 401 | Invalid or missing authentication token | | 403 | Permission denied | | 404 | Client not found | ## Related Endpoints - [List clients](/api-reference/clients/list) - [Bulk delete clients](/api-reference/clients/bulk-delete) --- ## Get client > Retrieve detailed information about a specific client including document history. URL: https://docs.storno.ro/api-reference/clients/get # Get client Retrieves detailed information about a specific client, including paginated invoice history, delivery note history, and receipt history. ```http GET /api/v1/clients/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the client | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `page` | integer | No | Page number for invoice history (default: 1) | | `limit` | integer | No | Items per page for invoice history (default: 50, max: 200) | ## Response Returns the full client object with document history. ### Response Schema | Field | Type | Description | |-------|------|-------------| | `client` | object | Full [client object](/objects/client) with all detail fields | | `invoiceHistory` | array | Paginated array of outgoing invoices for this client | | `invoiceTotal` | integer | Total number of outgoing invoices | | `invoiceCount` | integer | Total invoice count | | `deliveryNoteHistory` | array | Recent delivery notes (last 5) | | `deliveryNoteCount` | integer | Total delivery note count | | `receiptHistory` | array | Recent receipts (last 5) | | `receiptCount` | integer | Total receipt count | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/clients/b2c3d4e5-f6a7-8901-bcde-f12345678901' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json { "client": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "type": "company", "name": "Acme Corporation SRL", "cui": "12345678", "cnp": null, "vatCode": "RO12345678", "isVatPayer": true, "registrationNumber": "J40/1234/2020", "address": "Strada Exemplu, nr. 10", "city": "Bucuresti Sectorul 1", "county": "Bucuresti", "country": "RO", "postalCode": "010101", "email": "billing@acme.ro", "phone": "+40 21 123 4567", "bankName": "Banca Transilvania", "bankAccount": "RO49AAAA1B31007593840000", "defaultPaymentTermDays": 30, "contactPerson": "Ion Popescu", "notes": null, "source": "manual", "viesValid": null, "viesValidatedAt": null, "viesName": null, "lastSyncedAt": null, "createdAt": "2025-06-10T09:00:00+02:00", "updatedAt": "2026-02-10T14:30:00+02:00" }, "invoiceHistory": [ { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "number": "FAC0245", "issueDate": "2026-02-10", "total": "2380.00", "currency": "RON", "status": "issued", "direction": "outgoing" } ], "invoiceTotal": 24, "invoiceCount": 24, "deliveryNoteHistory": [], "deliveryNoteCount": 0, "receiptHistory": [], "receiptCount": 0 } ``` ## Errors | Status Code | Description | |-------------|-------------| | 401 | Invalid or missing authentication token | | 403 | Permission denied | | 404 | Client not found | ## Related Endpoints - [List clients](/api-reference/clients/list) - [Update client](/api-reference/clients/update) - [Delete client](/api-reference/clients/delete) --- ## List clients > Retrieve a paginated, alphabetically grouped list of clients. URL: https://docs.storno.ro/api-reference/clients/list # List clients Retrieves a paginated list of clients for the authenticated company, grouped alphabetically by the first letter of their name. Results can be filtered by type and searched by name or tax identification number. ```http GET /api/v1/clients ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `page` | integer | No | Page number (default: 1) | | `limit` | integer | No | Items per page (default: 50, max: 200) | | `search` | string | No | Search term to filter by name, CUI/CNP, or email | | `type` | string | No | Filter by type: `company` or `individual` | ## Response Returns a paginated object with clients grouped alphabetically. ### Response Schema | Field | Type | Description | |-------|------|-------------| | `data` | object | Object with alphabetic keys (A-Z, #) containing arrays of clients | | `total` | integer | Total number of clients matching filters | | `page` | integer | Current page number | | `limit` | integer | Items per page | | `pages` | integer | Total number of pages | ### Client Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `name` | string | Client name or full name | | `type` | string | `company` or `individual` | | `cui` | string \| null | Tax identification number (CUI for companies, CNP for individuals) | | `tradeRegister` | string \| null | Trade register number (J##/####/####) | | `email` | string \| null | Email address | | `phone` | string \| null | Phone number | | `address` | string \| null | Street address | | `city` | string \| null | City | | `county` | string \| null | County | | `country` | string | Country code (default: "RO") | | `viesValid` | boolean \| null | VIES validation result for EU clients (`true` = valid, `false` = invalid, `null` = not validated) | | `postalCode` | string \| null | Postal code | | `bankName` | string \| null | Bank name | | `bankAccount` | string \| null | Bank account (IBAN) | | `invoiceCount` | integer | Number of invoices issued to this client | | `totalRevenue` | number | Total revenue from this client | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/clients?page=1&limit=50&type=company' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json { "data": { "A": [ { "uuid": "client-uuid-1", "name": "Acme Corporation SRL", "type": "company", "cui": "RO12345678", "tradeRegister": "J40/1234/2020", "email": "contact@acme.ro", "phone": "+40723456789", "address": "Str. Exemplu, Nr. 1", "city": "București", "county": "București", "country": "RO", "postalCode": "010101", "bankName": "BCR", "bankAccount": "RO49AAAA1B31007593840000", "invoiceCount": 24, "totalRevenue": 42780.00, "createdAt": "2025-06-10T09:00:00Z", "updatedAt": "2026-02-10T14:30:00Z" }, { "uuid": "client-uuid-2", "name": "Alpha Tech SRL", "type": "company", "cui": "RO87654321", "tradeRegister": "J12/5678/2019", "email": "office@alphatech.ro", "phone": "+40734567890", "address": "Bd. Tehnologiei, Nr. 45", "city": "Cluj-Napoca", "county": "Cluj", "country": "RO", "postalCode": "400001", "bankName": "BT", "bankAccount": "RO49BTRL01101205N50289XX", "invoiceCount": 12, "totalRevenue": 18450.00, "createdAt": "2025-08-15T11:20:00Z", "updatedAt": "2026-01-25T16:45:00Z" } ], "B": [ { "uuid": "client-uuid-3", "name": "Beta Solutions SRL", "type": "company", "cui": "RO11223344", "tradeRegister": "J23/9012/2021", "email": "contact@betasolutions.ro", "phone": "+40745678901", "address": "Str. Inovației, Nr. 78", "city": "Ilfov", "county": "Ilfov", "country": "RO", "postalCode": "077190", "bankName": "ING", "bankAccount": "RO49INGB0000999900000017", "invoiceCount": 6, "totalRevenue": 9200.00, "createdAt": "2025-09-22T10:15:00Z", "updatedAt": "2026-02-05T12:00:00Z" } ] }, "total": 47, "page": 1, "limit": 50, "pages": 1 } ``` ## Alphabetical Grouping Clients are grouped by the first character of their name: - Letters A-Z: Standard alphabetic grouping - "#": Used for names starting with numbers or special characters If a letter has no clients, it will not appear in the response object. ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 422 | validation_error | Invalid query parameters | ## Related Endpoints - [Get client](/api-reference/clients/get) - [Sync clients from ANAF](/api-reference/anaf/sync-invoices) --- ## Update client > Update an existing client's details URL: https://docs.storno.ro/api-reference/clients/update # Update client Updates an existing client. All fields are optional — only include the fields you want to change. When identity or VAT-related fields are updated (`name`, `cui`, `cnp`, `vatCode`, `isVatPayer`, `country`), the changes are automatically propagated to all **editable invoices from the current month**. This updates the receiver name and CIF on those invoices and reapplies VAT rules (reverse charge / OSS). Past months' invoices are not affected. When `vatCode` or `country` is changed, the system also re-validates the VAT number against the EU VIES system for foreign EU clients. ```http PATCH /api/v1/clients/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the client | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Request body | Name | Type | Description | |------|------|-------------| | `name` | string | Company name or individual full name | | `type` | string | Client type: `company` or `individual` | | `cui` | string | CUI (tax identification number) | | `cnp` | string | CNP (personal identification number) | | `vatCode` | string | Full VAT code with country prefix | | `isVatPayer` | boolean | Whether the client is registered for VAT | | `registrationNumber` | string | Company registration number | | `address` | string | Street address | | `city` | string | City name | | `county` | string | County | | `country` | string | ISO 3166-1 alpha-2 country code | | `postalCode` | string | Postal/ZIP code | | `email` | string | Email address | | `phone` | string | Phone number | | `bankName` | string | Bank name | | `bankAccount` | string | Bank account number (IBAN) | | `defaultPaymentTermDays` | integer | Default payment term in days | | `contactPerson` | string | Contact person name | | `notes` | string | Internal notes | | `idNumber` | string | Client identification number (personal ID, passport, etc.) | | `currency` | string | Preferred currency for this client (ISO 4217: EUR, USD, RON, etc.) | ## Example Request ```bash curl -X PATCH 'https://api.storno.ro/api/v1/clients/b2c3d4e5-f6a7-8901-bcde-f12345678901' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "vatCode": "DE812526315", "isVatPayer": true, "country": "DE" }' ``` ## Response Returns the updated client object along with the number of invoices that were automatically updated. ```json { "client": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "type": "company", "name": "Example GmbH", "vatCode": "DE812526315", "isVatPayer": true, "country": "DE", "viesValid": true, "viesValidatedAt": "2026-03-02T12:00:00+02:00", "viesName": "EXAMPLE GMBH", ... }, "invoicesUpdated": 3 } ``` ### Invoice propagation The `invoicesUpdated` field indicates how many invoices were automatically updated. Propagation only affects invoices that are: - **Editable** — not cancelled, not sent to ANAF provider, and either draft, rejected, or issued but not yet uploaded to ANAF - **Current month** — `issueDate` is in the current calendar month (past months are considered fiscally closed) For each affected invoice, the system: 1. Updates `receiverName` and `receiverCif` from the new client data 2. Reapplies reverse charge / OSS VAT rules on invoice lines 3. Recalculates invoice totals 4. Invalidates cached XML (will be regenerated on next PDF/XML request) ## Errors | Status Code | Description | |-------------|-------------| | 401 | Invalid or missing authentication token | | 403 | Permission denied | | 404 | Client not found | | 422 | Validation error (empty required fields, missing registration number for RO companies) | ## Related Endpoints - [Get client](/api-reference/clients/get) - [VIES lookup](/api-reference/clients/vies-lookup) - [Update invoice](/api-reference/invoices/update) --- ## VIES lookup > Validate a VAT code against the EU VIES system URL: https://docs.storno.ro/api-reference/clients/vies-lookup # VIES lookup Validates a VAT code against the EU VIES (VAT Information Exchange System) API. Returns whether the VAT number is valid and the registered company name/address. Use this to verify EU intra-community VAT numbers before applying reverse charge. ```http GET /api/v1/clients/vies-lookup ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `vatCode` | string | Yes | Full EU VAT code including country prefix (e.g., `DE123456789`, `FR12345678901`) | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/clients/vies-lookup?vatCode=DE812526315' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json { "data": { "valid": true, "name": "EXAMPLE GMBH", "address": "MUSTERSTRASSE 1, 10115 BERLIN", "countryCode": "DE", "vatNumber": "812526315" } } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `valid` | boolean | Whether the VAT number is valid and active | | `name` | string \| null | Company name as registered in the VIES system | | `address` | string \| null | Registered address | | `countryCode` | string | Two-letter EU country code | | `vatNumber` | string | VAT number without country prefix | ## Auto-validation When a foreign EU client is created or updated with a VAT code, the system automatically validates it against VIES. The result is stored in the client's `viesValid`, `viesValidatedAt`, and `viesName` fields. This automatic validation only applies to EU member state country codes — non-EU countries (US, UK, CH, etc.) are skipped. ## Errors | Status Code | Description | |-------------|-------------| | 401 | Invalid or missing authentication token | | 422 | Missing or invalid VAT code format | | 503 | VIES service unavailable | ## Related Endpoints - [List clients](/api-reference/clients/list) - [Get client](/api-reference/clients/get) --- ## Create Company > Add a new company by CIF with automatic ANAF validation URL: https://docs.storno.ro/api-reference/companies/create # Create Company Creates a new company by providing its CIF (tax identification number). The system automatically validates the CIF with ANAF and retrieves the company's official registration data including name, address, VAT status, and other details. ## Headers | Header | Required | Description | |--------|----------|-------------| | Authorization | Yes | Bearer {token} | ## Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | cif | string | Yes | The CIF/tax ID (e.g., "RO12345678" or "12345678") | ## Request ```bash curl -X POST https://api.storno.ro/api/v1/companies \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "cif": "12345678" }' ``` ```js const response = await fetch('https://api.storno.ro/api/v1/companies', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_JWT_TOKEN', 'Content-Type': 'application/json' }, body: JSON.stringify({ cif: '12345678' }) }); const company = await response.json(); ``` ## Response ```json { "uuid": "550e8400-e29b-41d4-a716-446655440000", "name": "SRL Example Company", "cif": "12345678", "registrationNumber": "J40/1234/2020", "vatPayer": true, "vatCode": "RO12345678", "address": "Strada Exemplu, Nr. 10", "city": "Bucuresti", "state": "Bucuresti", "country": "Romania", "sector": "Sector 1", "phone": null, "email": null, "bankName": null, "bankAccount": null, "bankBic": null, "defaultCurrency": "RON", "syncEnabled": false, "lastSyncedAt": null, "syncDaysBack": 30, "efacturaDelayHours": 24, "archiveEnabled": false, "archiveRetentionYears": 10, "tokenStatus": { "hasToken": false, "isValid": false, "expiresAt": null } } ``` ## Error Codes | Code | Description | |------|-------------| | 400 | Invalid CIF format or ANAF validation failed | | 401 | Unauthorized - Invalid or missing token | | 403 | Forbidden - No access | | 409 | Conflict - Company with this CIF already exists in organization | | 500 | Internal server error | --- ## Delete Company > Permanently delete a company and all associated data URL: https://docs.storno.ro/api-reference/companies/delete # Delete Company Permanently deletes a company and dispatches an asynchronous cascade deletion of all associated resources including invoices, clients, products, ANAF tokens, and other company-specific data. Only users with Owner or Admin roles can delete companies. ## Headers | Header | Required | Description | |--------|----------|-------------| | Authorization | Yes | Bearer {token} | ## Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | uuid | string | Company UUID | ## Request ```bash curl -X DELETE https://api.storno.ro/api/v1/companies/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ```js const companyUuid = '550e8400-e29b-41d4-a716-446655440000'; const response = await fetch(`https://api.storno.ro/api/v1/companies/${companyUuid}`, { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_JWT_TOKEN' } }); // Response status: 204 No Content ``` ## Response No content is returned. HTTP status 204 indicates successful deletion. ## Error Codes | Code | Description | |------|-------------| | 401 | Unauthorized - Invalid or missing token | | 403 | Forbidden - Insufficient permissions (only Owner/Admin can delete) | | 404 | Not Found - Company does not exist | | 500 | Internal server error | --- ## Get Company > Retrieve detailed information for a specific company URL: https://docs.storno.ro/api-reference/companies/get # Get Company Returns detailed information for a specific company, including all configuration settings and ANAF token status. ## Headers | Header | Required | Description | |--------|----------|-------------| | Authorization | Yes | Bearer {token} | ## Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | uuid | string | Company UUID | ## Request ```bash curl -X GET https://api.storno.ro/api/v1/companies/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ```js const companyUuid = '550e8400-e29b-41d4-a716-446655440000'; const response = await fetch(`https://api.storno.ro/api/v1/companies/${companyUuid}`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_JWT_TOKEN' } }); const company = await response.json(); ``` ## Response ```json { "uuid": "550e8400-e29b-41d4-a716-446655440000", "name": "SRL Example Company", "cif": "12345678", "registrationNumber": "J40/1234/2020", "vatPayer": true, "vatCode": "RO12345678", "address": "Strada Exemplu, Nr. 10", "city": "Bucuresti", "state": "Bucuresti", "country": "Romania", "sector": "Sector 1", "phone": "+40721234567", "email": "contact@example.ro", "bankName": "Banca Transilvania", "bankAccount": "RO49AAAA1B31007593840000", "bankBic": "BTRLRO22", "defaultCurrency": "RON", "syncEnabled": true, "lastSyncedAt": "2026-02-16T10:30:00Z", "syncDaysBack": 30, "efacturaDelayHours": 24, "archiveEnabled": true, "archiveRetentionYears": 10, "enabledModules": null, "tokenStatus": { "hasToken": true, "isValid": true, "expiresAt": "2026-03-16T10:30:00Z" } } ``` The `enabledModules` field controls sidebar/menu visibility for optional modules. When `null`, all modules are visible (default). When set to an array, only those module keys appear in the navigation. Valid keys: `delivery_notes`, `receipts`, `proforma_invoices`, `recurring_invoices`, `reports`, `efactura`, `spv_messages`. ## Error Codes | Code | Description | |------|-------------| | 401 | Unauthorized - Invalid or missing token | | 403 | Forbidden - No access to this company | | 404 | Not Found - Company does not exist | | 500 | Internal server error | --- ## List Companies > Retrieve all companies for the current organization URL: https://docs.storno.ro/api-reference/companies/list # List Companies Returns an array of all companies belonging to the authenticated user's organization. Each company includes its basic information, configuration settings, and ANAF token status. ## Headers | Header | Required | Description | |--------|----------|-------------| | Authorization | Yes | Bearer {token} | ## Request ```bash curl -X GET https://api.storno.ro/api/v1/companies \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ```js const response = await fetch('https://api.storno.ro/api/v1/companies', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_JWT_TOKEN' } }); const companies = await response.json(); ``` ## Response ```json [ { "uuid": "550e8400-e29b-41d4-a716-446655440000", "name": "SRL Example Company", "cif": "12345678", "registrationNumber": "J40/1234/2020", "vatPayer": true, "vatCode": "RO12345678", "address": "Strada Exemplu, Nr. 10", "city": "Bucuresti", "state": "Bucuresti", "country": "Romania", "sector": "Sector 1", "phone": "+40721234567", "email": "contact@example.ro", "bankName": "Banca Transilvania", "bankAccount": "RO49AAAA1B31007593840000", "bankBic": "BTRLRO22", "defaultCurrency": "RON", "syncEnabled": true, "lastSyncedAt": "2026-02-16T10:30:00Z", "syncDaysBack": 30, "efacturaDelayHours": 24, "archiveEnabled": true, "archiveRetentionYears": 10, "enabledModules": null, "tokenStatus": { "hasToken": true, "isValid": true, "expiresAt": "2026-03-16T10:30:00Z" } } ] ``` ## Error Codes | Code | Description | |------|-------------| | 401 | Unauthorized - Invalid or missing token | | 500 | Internal server error | --- ## Toggle Sync > Enable or disable ANAF e-Factura synchronization URL: https://docs.storno.ro/api-reference/companies/toggle-sync # Toggle Sync Toggles the ANAF e-Factura synchronization status for a company. Requires a valid ANAF OAuth token for the company's CIF. When enabled, the system will automatically sync invoices from e-Factura according to the configured sync settings. ## Headers | Header | Required | Description | |--------|----------|-------------| | Authorization | Yes | Bearer {token} | ## Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | uuid | string | Company UUID | ## Request ```bash curl -X POST https://api.storno.ro/api/v1/companies/550e8400-e29b-41d4-a716-446655440000/toggle-sync \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ```js const companyUuid = '550e8400-e29b-41d4-a716-446655440000'; const response = await fetch(`https://api.storno.ro/api/v1/companies/${companyUuid}/toggle-sync`, { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_JWT_TOKEN' } }); const company = await response.json(); ``` ## Response The endpoint returns the new sync state, not the full company resource. Read the full company via `GET /api/v1/companies/{uuid}` if you need the rest. ```json { "syncEnabled": true, "message": "Sync enabled" } ``` When sync was previously enabled the same call disables it: ```json { "syncEnabled": false, "message": "Sync disabled" } ``` ## Error Codes | Code | Description | |------|-------------| | 401 | Unauthorized — invalid or missing JWT | | 403 | Forbidden — caller lacks `COMPANY_EDIT` on the target company | | 404 | Not Found — company UUID does not exist | | 422 | Unprocessable Entity — attempted to **enable** sync but the company has no valid ANAF OAuth token. Response body: `{ "error": "...", "messageKey": "ERR_SYNC_ENABLE_NO_TOKEN" }`. Connect via the ANAF OAuth flow first (`POST /api/v1/anaf/token-links`) and try again. | | 500 | Internal server error | --- ## Update Company > Update company configuration settings URL: https://docs.storno.ro/api-reference/companies/update # Update Company Updates configuration settings for a company. Note that core ANAF data (CIF, registration number, VAT status, official address) cannot be modified as they are synced from official sources. ## Headers | Header | Required | Description | |--------|----------|-------------| | Authorization | Yes | Bearer {token} | ## Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | uuid | string | Company UUID | ## Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | name | string | No | Company display name | | bankName | string | No | Bank name | | bankAccount | string | No | IBAN account number | | bankBic | string | No | BIC/SWIFT code | | defaultCurrency | string | No | Default currency code (e.g., "RON", "EUR") | | phone | string | No | Contact phone number | | email | string | No | Contact email address | | syncDaysBack | integer | No | Number of days to sync back from ANAF (1-365) | | efacturaDelayHours | integer | No | Hours to delay e-Factura sync (0-72) | | archiveEnabled | boolean | No | Enable automatic archiving | | archiveRetentionYears | integer | No | Years to retain archived data (1-50) | | enabledModules | string[] \| null | No | Array of enabled module keys for sidebar visibility. `null` = all enabled. Valid keys: `delivery_notes`, `receipts`, `proforma_invoices`, `recurring_invoices`, `reports`, `efactura`, `spv_messages`. When all 7 keys are provided, the server stores `null`. | ## Request ```bash curl -X PATCH https://api.storno.ro/api/v1/companies/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "phone": "+40721234567", "email": "updated@example.ro", "bankName": "Banca Transilvania", "bankAccount": "RO49AAAA1B31007593840000", "syncDaysBack": 60 }' ``` ```js const companyUuid = '550e8400-e29b-41d4-a716-446655440000'; const response = await fetch(`https://api.storno.ro/api/v1/companies/${companyUuid}`, { method: 'PATCH', headers: { 'Authorization': 'Bearer YOUR_JWT_TOKEN', 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: '+40721234567', email: 'updated@example.ro', bankName: 'Banca Transilvania', bankAccount: 'RO49AAAA1B31007593840000', syncDaysBack: 60 }) }); const company = await response.json(); ``` ## Response ```json { "uuid": "550e8400-e29b-41d4-a716-446655440000", "name": "SRL Example Company", "cif": "12345678", "registrationNumber": "J40/1234/2020", "vatPayer": true, "vatCode": "RO12345678", "address": "Strada Exemplu, Nr. 10", "city": "Bucuresti", "state": "Bucuresti", "country": "Romania", "sector": "Sector 1", "phone": "+40721234567", "email": "updated@example.ro", "bankName": "Banca Transilvania", "bankAccount": "RO49AAAA1B31007593840000", "bankBic": "BTRLRO22", "defaultCurrency": "RON", "syncEnabled": true, "lastSyncedAt": "2026-02-16T10:30:00Z", "syncDaysBack": 60, "efacturaDelayHours": 24, "archiveEnabled": true, "archiveRetentionYears": 10, "enabledModules": null, "tokenStatus": { "hasToken": true, "isValid": true, "expiresAt": "2026-03-16T10:30:00Z" } } ``` ## Error Codes | Code | Description | |------|-------------| | 400 | Bad Request - Validation failed for provided parameters | | 401 | Unauthorized - Invalid or missing token | | 403 | Forbidden - No access to this company | | 404 | Not Found - Company does not exist | | 500 | Internal server error | --- ## Create Credit Note > Create a new credit note to reverse or partially reverse an invoice URL: https://docs.storno.ro/api-reference/credit-notes/create # Create Credit Note Creates a new credit note that reverses or partially reverses an original invoice. Credit notes are created using the same endpoint as invoices but with `isCreditNote: true` and a reference to the parent invoice. Credit notes are not the only way to issue refunds. You can also create a regular invoice with negative quantities — no `parentDocumentId` or `isCreditNote` flag needed. See the [Refund an invoice](/api-reference/invoices/refund) guide for a comparison of both approaches. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `isCreditNote` | boolean | Yes | Must be `true` for credit notes | | `direction` | string | Yes | Must be `outgoing` for credit notes | | `parentDocumentId` | string | Yes | UUID of the original invoice being credited | | `clientId` | string | Yes | UUID of the client (must match parent invoice) | | `seriesId` | string | Yes | UUID of the credit note series | | `issueDate` | string | Yes | Date of issue (YYYY-MM-DD) | | `dueDate` | string | Yes | Due date (YYYY-MM-DD) | | `currency` | string | Yes | Currency code (must match parent invoice) | | `exchangeRate` | number | No | Exchange rate (defaults to 1.0 for RON) | | `invoiceTypeCode` | string | No | Invoice type code (default: "381" - Credit Note) | | `notes` | string | No | Public notes explaining the credit | | `paymentTerms` | string | No | Payment/refund terms | | `issuerName` | string | No | Name of person issuing the credit note | | `issuerId` | string | No | UUID of the issuer user | | `mentions` | string | No | Additional mentions | | `internalNote` | string | No | Internal note (not visible to client) | | `lines` | array | Yes | Array of line items (minimum 1 item) | ### Line Item Object | Field | Type | Required | Description | |-------|------|----------|-------------| | `description` | string | Yes | Item description (typically from original invoice) | | `quantity` | number | Yes | **Negative** quantity or positive with negative unit price | | `unitPrice` | number | Yes | Unit price (positive if quantity is negative) | | `vatRateId` | string | Yes | UUID of the VAT rate (must match original) | | `unitOfMeasure` | string | No | Unit of measure | | `productId` | string | No | UUID of related product | | `discount` | number | No | Discount amount (positive, reduces credit) | | `discountPercent` | number | No | Discount percentage (0-100) | | `vatIncluded` | boolean | No | Whether unit price includes VAT (default: false) | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/invoices \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "isCreditNote": true, "direction": "outgoing", "parentDocumentId": "650e8400-e29b-41d4-a716-446655440111", "clientId": "750e8400-e29b-41d4-a716-446655440000", "seriesId": "750e8400-e29b-41d4-a716-446655440000", "issueDate": "2026-02-20", "dueDate": "2026-03-20", "currency": "RON", "exchangeRate": 1.0, "invoiceTypeCode": "381", "notes": "Partial refund - hosting services cancelled by client request", "paymentTerms": "Immediate refund", "issuerName": "John Doe", "mentions": "Credit note for invoice FAC-2026-045", "internalNote": "Client cancelled annual hosting, refund via original payment method", "lines": [ { "description": "Hosting Services - Annual (CREDIT)", "quantity": -1, "unitPrice": 1200, "unitOfMeasure": "service", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "460e8400-e29b-41d4-a716-446655440000", "discount": 200, "vatIncluded": false } ] }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/invoices', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ isCreditNote: true, direction: 'outgoing', parentDocumentId: '650e8400-e29b-41d4-a716-446655440111', clientId: '750e8400-e29b-41d4-a716-446655440000', seriesId: '750e8400-e29b-41d4-a716-446655440000', issueDate: '2026-02-20', dueDate: '2026-03-20', currency: 'RON', exchangeRate: 1.0, invoiceTypeCode: '381', notes: 'Partial refund - hosting services cancelled by client request', paymentTerms: 'Immediate refund', issuerName: 'John Doe', mentions: 'Credit note for invoice FAC-2026-045', internalNote: 'Client cancelled annual hosting, refund via original payment method', lines: [ { description: 'Hosting Services - Annual (CREDIT)', quantity: -1, unitPrice: 1200, unitOfMeasure: 'service', vatRateId: '350e8400-e29b-41d4-a716-446655440000', productId: '460e8400-e29b-41d4-a716-446655440000', discount: 200, vatIncluded: false } ] }) }); const data = await response.json(); ``` ## Response ```json { "uuid": "850e8400-e29b-41d4-a716-446655440000", "number": "CN-2026-005", "direction": "outgoing", "isCreditNote": true, "seriesId": "750e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "CN", "nextNumber": 6, "prefix": "CN-", "year": 2026 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "address": "Str. Exemplu 123, București", "email": "contact@client.ro" }, "parentDocumentId": "650e8400-e29b-41d4-a716-446655440111", "parentDocument": { "uuid": "650e8400-e29b-41d4-a716-446655440111", "number": "FAC-2026-045", "issueDate": "2026-02-18", "total": "8330.00" }, "status": "draft", "anafStatus": null, "issueDate": "2026-02-20", "dueDate": "2026-03-20", "currency": "RON", "exchangeRate": 1.0, "invoiceTypeCode": "381", "notes": "Partial refund - hosting services cancelled by client request", "paymentTerms": "Immediate refund", "issuerName": "John Doe", "mentions": "Credit note for invoice FAC-2026-045", "internalNote": "Client cancelled annual hosting, refund via original payment method", "lines": [ { "uuid": "910e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Hosting Services - Annual (CREDIT)", "quantity": "-1.00", "unitPrice": "1200.00", "unitOfMeasure": "service", "productId": "460e8400-e29b-41d4-a716-446655440000", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "discount": "200.00", "vatIncluded": false, "subtotal": "-1000.00", "vatAmount": "-190.00", "total": "-1190.00" } ], "subtotal": "-1000.00", "totalDiscount": "200.00", "vatAmount": "-190.00", "total": "-1190.00", "anafUploadIndex": null, "createdAt": "2026-02-20T09:00:00Z", "updatedAt": "2026-02-20T09:00:00Z" } ``` ## Validation Rules ### Parent Invoice - `parentDocumentId` must reference an existing invoice - Parent invoice must belong to the same company - Parent invoice must be validated by ANAF (recommended) - Parent invoice cannot be a credit note itself ### Client Match - `clientId` must match the parent invoice's client - Cannot create credit note for different client ### Currency Match - `currency` must match parent invoice currency - `exchangeRate` should match parent invoice (if applicable) ### Amounts - All calculated amounts (subtotal, vat, total) will be negative - Line item quantities should be negative (or unit prices negative) - Total credit should not exceed original invoice (warning, not error) ### Dates - `issueDate` should be on or after parent invoice `issueDate` - `issueDate` should not be in the future - `dueDate` must be equal to or after `issueDate` ### Line Items - Minimum 1 line item required - Line totals must be negative - VAT rates must match those used in parent invoice (recommended) ### Series - `seriesId` must reference a series configured for credit notes - Series must belong to the same company ## Invoice Type Codes Common type codes for credit notes: - **381** - Credit note (most common) - **383** - Corrective invoice - **384** - Corrected invoice Default is 381 if not specified. ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid request body structure | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Parent invoice, client, series, or VAT rate not found | | 422 | `validation_error` | Validation failed (see error details) | | 500 | `internal_error` | Server error occurred | ## Example Validation Error Response ```json { "error": { "code": "validation_error", "message": "Validation failed", "details": { "parentDocumentId": ["Parent invoice not found"], "clientId": ["Client must match parent invoice client"], "currency": ["Currency must match parent invoice currency"], "lines.0.quantity": ["Quantity must be negative for credit notes"], "issueDate": ["Issue date must be after parent invoice date"] } } } ``` ## Credit Note Scenarios ### Full Refund Credit all line items from original invoice: ```javascript { // ... other fields lines: [ { quantity: -40, unitPrice: 150, ... }, // All web dev hours { quantity: -1, unitPrice: 1200, ... } // All hosting ] // Result: -8,330.00 RON (full credit) } ``` ### Partial Refund Credit only specific line items: ```javascript { // ... other fields lines: [ { quantity: -1, unitPrice: 1200, ... } // Only hosting ] // Result: -1,190.00 RON (partial credit) } ``` ### Quantity Adjustment Credit partial quantity of a line: ```javascript { // ... other fields lines: [ { quantity: -10, unitPrice: 150, ... } // Only 10 of 40 hours ] // Result: -1,785.00 RON (partial credit) } ``` ### Error Correction Full credit + new invoice with correct amounts: ```javascript // Step 1: Credit Note (full) { lines: [{ quantity: -40, unitPrice: 150, ... }] } // Step 2: New Invoice (corrected) { lines: [{ quantity: 45, unitPrice: 150, ... }] } ``` ## Best Practices 1. **Reference original clearly** - Include original invoice number in notes 2. **Explain the reason** - Clear notes about why credit is issued 3. **Use negative quantities** - More intuitive than negative prices 4. **Match original details** - Use same products, VAT rates when possible 5. **Check parent status** - Ensure parent invoice is validated first 6. **Upload promptly** - Submit to ANAF after creation 7. **Notify client** - Send credit note with clear explanation 8. **Track cumulative credits** - Monitor total credited per invoice 9. **Coordinate refunds** - Link credit note to actual payment refund 10. **Update accounting** - Sync to accounting system immediately ## Next Steps After creating a credit note: 1. Upload to ANAF (`POST /api/v1/invoices/{uuid}/upload`) 2. Generate PDF for client 3. Send to client via email 4. Process refund payment if applicable 5. Update accounting records 6. Track against original invoice --- ## Delete Credit Note > Permanently delete a credit note (draft status only) URL: https://docs.storno.ro/api-reference/credit-notes/delete # Delete Credit Note Permanently deletes a credit note and all its line items. Only credit notes in `draft` status can be deleted. Once uploaded to ANAF, a credit note cannot be deleted. Credit notes use the same delete endpoint as invoices. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the credit note to delete | ## Request ```bash {% title="cURL" %} curl -X DELETE https://api.storno.ro/api/v1/invoices/850e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/invoices/850e8400-e29b-41d4-a716-446655440000', { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); // Success: 204 No Content (no response body) if (response.status === 204) { console.log('Credit note deleted successfully'); } ``` ## Response **Success:** Returns `204 No Content` with an empty response body. The credit note and all associated line items are permanently deleted from the database. ## Restrictions ### Status Requirement Only credit notes with `status = draft` can be deleted. Credit notes in the following states **cannot** be deleted: - `uploaded` - Already submitted to ANAF - `validated` - ANAF has validated it - `error` - Upload failed but record must be kept for troubleshooting ### Referential Integrity - Deleting a credit note does not affect the parent invoice - The credit note number series counter is not rolled back - Parent invoice's credited amount tracking is updated ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Credit note not found or doesn't belong to the company | | 409 | `conflict` | Credit note status prevents deletion (not in draft) | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict - Uploaded ```json { "error": { "code": "conflict", "message": "Credit note cannot be deleted", "details": { "status": "uploaded", "reason": "Only draft credit notes can be deleted. This credit note has been uploaded to ANAF.", "uploadedAt": "2026-02-20T10:00:00Z", "anafUploadIndex": 2026000234 } } } ``` ### Status Conflict - Validated ```json { "error": { "code": "conflict", "message": "Credit note cannot be deleted", "details": { "status": "validated", "reason": "Only draft credit notes can be deleted. This credit note has been validated by ANAF.", "validatedAt": "2026-02-20T10:15:00Z", "anafUploadIndex": 2026000234 } } } ``` ### Not Found ```json { "error": { "code": "not_found", "message": "Credit note not found" } } ``` ## Best Practices ### When to Delete **Delete** when: - The credit note was created by mistake - The credit note is still in draft and hasn't been uploaded - You want to remove all traces of the document - Wrong parent invoice was referenced - Amounts were completely incorrect and easier to recreate ### When NOT to Delete **Do NOT delete** when: - Credit note has been uploaded to ANAF - Credit note has been sent to client (even if draft) - You need audit trail - You need historical records for reporting - Credit note was shared externally ### Alternatives to Deletion #### For Uploaded Credit Notes If credit note is already uploaded and you need to reverse it: 1. **Cannot delete** - Deletion is not possible 2. **Issue corrective credit note** - Create opposite credit note (positive amounts) to reverse 3. **Document in notes** - Add explanation in accounting records 4. **Consult accountant** - Get guidance on proper reversal procedure #### For Draft Credit Notes with History If credit note is draft but has been communicated: 1. **Delete if appropriate** - If not shared externally 2. **Keep for audit** - If there's any external communication 3. **Create corrected version** - Delete and recreate with correct data 4. **Document reason** - Log why deletion was necessary ## Audit Considerations Deleted credit notes: - Are permanently removed from the database - Do not appear in reports or exports - Cannot be recovered - Do not leave audit trail entries - Do not affect ANAF records (since never uploaded) For compliance and audit purposes: - Only delete draft credit notes - Log deletion action in external system if needed - Consider keeping screenshot or PDF if shared with client - Document business reason for deletion ## Impact on Parent Invoice When a draft credit note is deleted: - Parent invoice is **not affected** - Parent invoice credited amount tracking is updated - Parent invoice remains valid and unchanged - Can create new credit note against same parent invoice ## Deletion vs Reversal ### Deletion (Draft Only) - **Scope:** Draft credit notes only - **Effect:** Permanent removal - **Audit trail:** None (record removed) - **ANAF impact:** None (never uploaded) - **Use when:** Created by mistake, not shared ### Reversal (Uploaded/Validated) - **Scope:** Any status - **Effect:** Create opposite entry - **Audit trail:** Both records preserved - **ANAF impact:** New submission required - **Use when:** Need to undo validated credit note ## Recovery from Accidental Deletion If you accidentally delete a draft credit note: 1. **Cannot recover** - Deletion is permanent 2. **Recreate from parent invoice** - Use parent invoice data 3. **Check backups** - If database backups exist 4. **Retrieve from PDF** - If PDF was generated 5. **Check email history** - If sent to client ## Common Scenarios ### Scenario 1: Wrong Parent Invoice ``` Problem: Created credit note against wrong invoice Solution: Delete draft credit note, create new one with correct parent ``` ### Scenario 2: Incorrect Amount ``` Problem: Used wrong line items or quantities Solution: Delete draft credit note, recreate with correct amounts ``` ### Scenario 3: Duplicate Creation ``` Problem: Accidentally created same credit note twice Solution: Delete the duplicate draft credit note ``` ### Scenario 4: Client Cancelled Request ``` Problem: Client no longer wants refund Solution: Delete draft credit note before upload ``` ## Best Practices Summary 1. **Delete only drafts** - Never attempt to delete uploaded credit notes 2. **Verify before deleting** - Double-check you're deleting the right document 3. **Log the action** - Record why credit note was deleted in external system 4. **Recreate if needed** - Create new correct credit note immediately 5. **Communicate changes** - If credit note was discussed with client, inform them 6. **Check parent status** - Ensure parent invoice is still valid 7. **Review process** - Understand why incorrect credit note was created ## After Deletion Once deleted: 1. **Verify deletion** - Check that credit note is removed from list 2. **Create replacement** - If credit is still needed 3. **Update documentation** - Note the deletion in related records 4. **Notify stakeholders** - If credit note was expected by others 5. **Review workflow** - Prevent similar errors in future --- ## Get Credit Note > Retrieve detailed information for a specific credit note including line items URL: https://docs.storno.ro/api-reference/credit-notes/get # Get Credit Note Retrieves complete details for a specific credit note, including all line items, client information, parent invoice reference, and calculated totals. Credit notes use the same endpoint as invoices since they are a special type of invoice. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the credit note to retrieve | ## Request ```bash {% title="cURL" %} curl -X GET https://api.storno.ro/api/v1/invoices/850e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/invoices/850e8400-e29b-41d4-a716-446655440000', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response ```json { "uuid": "850e8400-e29b-41d4-a716-446655440000", "number": "CN-2026-005", "direction": "outgoing", "isCreditNote": true, "seriesId": "750e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "CN", "nextNumber": 6, "prefix": "CN-", "year": 2026 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "address": "Str. Exemplu 123, București, Sector 1", "city": "București", "county": "București", "country": "RO", "postalCode": "010101", "email": "contact@client.ro", "phone": "+40721234567", "bankAccount": "RO49AAAA1B31007593840000", "bankName": "Banca Comercială Română" }, "parentDocumentId": "650e8400-e29b-41d4-a716-446655440111", "parentDocument": { "uuid": "650e8400-e29b-41d4-a716-446655440111", "number": "FAC-2026-045", "issueDate": "2026-02-18", "dueDate": "2026-03-18", "currency": "RON", "subtotal": "7000.00", "vatAmount": "1330.00", "total": "8330.00", "status": "validated", "anafUploadIndex": 2026000230 }, "status": "validated", "anafStatus": "validated", "anafUploadIndex": 2026000234, "anafCif": 2026000234, "issueDate": "2026-02-20", "dueDate": "2026-03-20", "currency": "RON", "exchangeRate": 1.0, "invoiceTypeCode": "381", "notes": "Partial refund - hosting services cancelled by client request", "paymentTerms": "Immediate refund", "issuerName": "John Doe", "issuerId": "850e8400-e29b-41d4-a716-446655440000", "mentions": "Credit note for invoice FAC-2026-045", "internalNote": "Client cancelled annual hosting, refund via original payment method", "lines": [ { "uuid": "910e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Hosting Services - Annual (CREDIT)", "quantity": "-1.00", "unitPrice": "1200.00", "unitOfMeasure": "service", "productId": "460e8400-e29b-41d4-a716-446655440000", "product": { "uuid": "460e8400-e29b-41d4-a716-446655440000", "name": "Hosting Services", "code": "HOST-001", "unitOfMeasure": "service" }, "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "discount": "200.00", "discountPercent": "16.67", "vatIncluded": false, "subtotal": "-1000.00", "vatAmount": "-190.00", "total": "-1190.00" } ], "subtotal": "-1000.00", "totalDiscount": "200.00", "vatAmount": "-190.00", "total": "-1190.00", "xmlContent": "...", "anafMessages": [], "createdAt": "2026-02-20T09:00:00Z", "updatedAt": "2026-02-20T10:15:00Z", "uploadedAt": "2026-02-20T09:30:00Z", "validatedAt": "2026-02-20T10:15:00Z" } ``` ## Response Fields ### Core Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `number` | string | Credit note number | | `isCreditNote` | boolean | Always `true` for credit notes | | `direction` | string | Always `outgoing` for credit notes | | `parentDocumentId` | string | UUID of the original invoice being credited | | `parentDocument` | object | Complete details of the original invoice | ### Status Fields | Field | Type | Description | |-------|------|-------------| | `status` | string | Document status: `draft`, `uploaded`, `validated`, `error` | | `anafStatus` | string | ANAF validation status | | `anafUploadIndex` | integer \| null | ANAF upload index (CIF) | ### Financial Fields | Field | Type | Description | |-------|------|-------------| | `subtotal` | string | **Negative** subtotal amount | | `totalDiscount` | string | Discount amount (positive, reduces the credit) | | `vatAmount` | string | **Negative** VAT amount | | `total` | string | **Negative** total amount | | `currency` | string | Currency code (must match parent invoice) | | `exchangeRate` | number | Exchange rate | ### Line Items | Field | Type | Description | |-------|------|-------------| | `lines` | array | Array of line items with negative amounts | | `quantity` | string | **Negative** quantity or positive with negative unit price | | `subtotal` | string | **Negative** line subtotal | | `vatAmount` | string | **Negative** line VAT | | `total` | string | **Negative** line total | ### Timestamps | Field | Type | Description | |-------|------|-------------| | `createdAt` | string | When credit note was created | | `uploadedAt` | string \| null | When uploaded to ANAF | | `validatedAt` | string \| null | When ANAF validated it | | `updatedAt` | string | Last update timestamp | ## Parent Document Reference The `parentDocument` object contains key information from the original invoice: - Original invoice number - Original issue date - Original amounts (positive) - ANAF upload status This allows you to: - Display both documents side-by-side - Validate the credit doesn't exceed the original - Track total credited amount per invoice - Generate reports on credits per invoice ## Credit Note Amounts ### Amount Sign Convention All financial amounts in credit notes are **negative**: ```json { "subtotal": "-1000.00", // Negative "vatAmount": "-190.00", // Negative "total": "-1190.00" // Negative } ``` ### Line Item Amounts Two ways to represent negative line amounts: **Option 1: Negative quantity** ```json { "quantity": "-1.00", "unitPrice": "1200.00", "total": "-1190.00" } ``` **Option 2: Negative unit price** ```json { "quantity": "1.00", "unitPrice": "-1200.00", "total": "-1190.00" } ``` Both are valid; negative quantity is more common. ## Invoice Type Code Credit notes typically use: - **381** - Credit note (most common) - **383** - Corrective invoice (for corrections) ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Credit note not found or doesn't belong to the company | | 500 | `internal_error` | Server error occurred | ## Validation Rules ### Amount Validation - Total credit amount should not exceed original invoice amount - Multiple credit notes can reference the same parent invoice - System tracks cumulative credited amount per invoice ### Date Validation - Credit note `issueDate` should be after parent invoice `issueDate` - Cannot create credit note for future-dated invoice - Must be within fiscal period rules ### Currency Match - Credit note currency must match parent invoice currency - Exchange rate should match parent invoice (if applicable) ## Use Cases ### Full Credit Credit entire original invoice: ``` Original Invoice: 8,330.00 RON Credit Note: -8,330.00 RON Net Balance: 0.00 RON ``` ### Partial Credit Credit specific line items: ``` Original Invoice: 8,330.00 RON (2 items) Credit Note: -1,190.00 RON (1 item) Net Balance: 7,140.00 RON ``` ### Multiple Credits Multiple partial credits on same invoice: ``` Original Invoice: 8,330.00 RON Credit Note 1: -1,190.00 RON Credit Note 2: -500.00 RON Net Balance: 6,640.00 RON ``` ## Best Practices 1. **Always link to parent** - Set `parentDocumentId` correctly 2. **Clear descriptions** - Explain what is being credited and why 3. **Match line items** - Use same product/description as original 4. **Check ANAF status** - Ensure parent invoice is validated first 5. **Notify client** - Send credit note with clear explanation 6. **Update accounting** - Sync to accounting system promptly 7. **Track totals** - Monitor total credited vs original amount --- ## List Credit Notes > Retrieve a paginated list of credit notes with optional filtering URL: https://docs.storno.ro/api-reference/credit-notes/list # List Credit Notes Retrieves a paginated list of credit notes for the authenticated company. Credit notes are special invoices that reverse or partially reverse original invoices. They share the same endpoint as invoices but are filtered using `isCreditNote=true`. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Query Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `direction` | string | Yes | Must be `outgoing` for credit notes | | `isCreditNote` | boolean | Yes | Must be `true` to filter for credit notes | | `page` | integer | No | Page number (default: 1) | | `limit` | integer | No | Items per page (default: 20, max: 100) | | `search` | string | No | Search term for credit note number or client name | | `status` | string | No | Filter by status: `draft`, `uploaded`, `validated`, `error` | | `from` | string | No | Start date filter (ISO 8601 format: YYYY-MM-DD) | | `to` | string | No | End date filter (ISO 8601 format: YYYY-MM-DD) | | `clientId` | string | No | Filter by client UUID | | `parentDocumentId` | string | No | Filter by parent invoice UUID | ## Request ```bash {% title="cURL" %} curl -X GET "https://api.storno.ro/api/v1/invoices?direction=outgoing&isCreditNote=true&page=1&limit=20" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/invoices?direction=outgoing&isCreditNote=true&page=1&limit=20', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response ```json { "data": [ { "uuid": "850e8400-e29b-41d4-a716-446655440000", "number": "CN-2026-005", "direction": "outgoing", "isCreditNote": true, "seriesId": "750e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "CN", "nextNumber": 6, "prefix": "CN-", "year": 2026 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "address": "Str. Exemplu 123, București" }, "parentDocumentId": "650e8400-e29b-41d4-a716-446655440111", "parentDocument": { "uuid": "650e8400-e29b-41d4-a716-446655440111", "number": "FAC-2026-045", "issueDate": "2026-02-18", "total": "8330.00" }, "status": "uploaded", "anafStatus": "validated", "issueDate": "2026-02-20", "dueDate": "2026-03-20", "currency": "RON", "exchangeRate": 1.0, "subtotal": "-1000.00", "vatAmount": "-190.00", "total": "-1190.00", "notes": "Partial refund - hosting services cancelled", "anafUploadIndex": 2026000234, "createdAt": "2026-02-20T09:00:00Z", "updatedAt": "2026-02-20T10:15:00Z" } ], "total": 12, "page": 1, "limit": 20, "pages": 1 } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `data` | array | Array of credit note objects | | `total` | integer | Total number of credit notes matching the filters | | `page` | integer | Current page number | | `limit` | integer | Items per page | | `pages` | integer | Total number of pages | ### Credit Note Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `number` | string | Credit note number (e.g., CN-2026-005) | | `isCreditNote` | boolean | Always `true` for credit notes | | `direction` | string | Always `outgoing` for credit notes | | `parentDocumentId` | string | UUID of the original invoice being credited | | `parentDocument` | object | Details of the original invoice | | `status` | string | Status: `draft`, `uploaded`, `validated`, `error` | | `anafStatus` | string | ANAF validation status | | `issueDate` | string | Date of issue (YYYY-MM-DD) | | `dueDate` | string | Due date (YYYY-MM-DD) | | `currency` | string | Currency code | | `subtotal` | string | Negative subtotal amount | | `vatAmount` | string | Negative VAT amount | | `total` | string | Negative total amount | | `client` | object | Client details | | `series` | object | Series details | | `anafUploadIndex` | integer \| null | ANAF upload index number | ## Credit Note Amounts Credit notes have **negative amounts** to indicate they reverse the original invoice: - `subtotal`, `vatAmount`, and `total` are negative - Line item amounts are negative - When applied, they reduce the client's outstanding balance ## Status Values Credit notes use the same statuses as invoices: - **draft** - Created but not yet uploaded to ANAF - **uploaded** - Uploaded to ANAF, awaiting validation - **validated** - ANAF validated the credit note - **error** - ANAF validation failed ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 422 | `validation_error` | Invalid query parameters | | 500 | `internal_error` | Server error occurred | ## Use Cases ### Full Refund Credit note with same amount as original invoice (negative): - Original invoice: 1,190.00 RON - Credit note: -1,190.00 RON - Net result: 0.00 RON ### Partial Refund Credit note with portion of original amount: - Original invoice: 8,330.00 RON - Credit note: -1,190.00 RON (one line item) - Net result: 7,140.00 RON ### Error Correction Credit original invoice, issue new corrected invoice: 1. Credit note: -8,330.00 RON (reverses entire original) 2. New invoice: 8,530.00 RON (correct amount) 3. Net result: +200.00 RON difference ## Best Practices 1. **Always reference parent** - Link credit note to original invoice 2. **Include clear notes** - Explain reason for credit note 3. **Upload promptly** - Submit to ANAF within required timeframe 4. **Track against original** - Monitor total credited amount vs original 5. **Notify client** - Send credit note to client with explanation 6. **Update accounting** - Sync to accounting system for proper recording --- ## Update Credit Note > Update an existing credit note (draft status only) URL: https://docs.storno.ro/api-reference/credit-notes/update # Update Credit Note Updates an existing credit note. Only credit notes in `draft` status can be updated. Once uploaded to ANAF, the credit note becomes immutable. Credit notes use the same update endpoint as invoices. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the credit note to update | ## Request Body All fields from the create endpoint can be updated. The entire credit note is replaced with the new data. | Field | Type | Required | Description | |-------|------|----------|-------------| | `isCreditNote` | boolean | Yes | Must be `true` | | `direction` | string | Yes | Must be `outgoing` | | `parentDocumentId` | string | Yes | UUID of the original invoice (cannot be changed) | | `clientId` | string | Yes | UUID of the client (must match parent) | | `seriesId` | string | Yes | UUID of the credit note series | | `issueDate` | string | Yes | Date of issue (YYYY-MM-DD) | | `dueDate` | string | Yes | Due date (YYYY-MM-DD) | | `currency` | string | Yes | Currency code (must match parent) | | `exchangeRate` | number | No | Exchange rate | | `invoiceTypeCode` | string | No | Invoice type code | | `notes` | string | No | Public notes | | `paymentTerms` | string | No | Payment/refund terms | | `issuerName` | string | No | Issuer name | | `issuerId` | string | No | Issuer UUID | | `mentions` | string | No | Additional mentions | | `internalNote` | string | No | Internal note | | `lines` | array | Yes | Array of line items (replaces all existing lines) | ### Line Item Object | Field | Type | Required | Description | |-------|------|----------|-------------| | `uuid` | string | No | UUID of existing line (if updating); omit to create new line | | `description` | string | Yes | Item description | | `quantity` | number | Yes | **Negative** quantity | | `unitPrice` | number | Yes | Unit price | | `vatRateId` | string | Yes | UUID of the VAT rate | | `unitOfMeasure` | string | No | Unit of measure | | `productId` | string | No | UUID of related product | | `discount` | number | No | Discount amount | | `discountPercent` | number | No | Discount percentage | | `vatIncluded` | boolean | No | Whether unit price includes VAT | ## Request ```bash {% title="cURL" %} curl -X PUT https://api.storno.ro/api/v1/invoices/850e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "isCreditNote": true, "direction": "outgoing", "parentDocumentId": "650e8400-e29b-41d4-a716-446655440111", "clientId": "750e8400-e29b-41d4-a716-446655440000", "seriesId": "750e8400-e29b-41d4-a716-446655440000", "issueDate": "2026-02-20", "dueDate": "2026-03-20", "currency": "RON", "notes": "Updated: Full refund - all services cancelled", "paymentTerms": "Immediate refund", "issuerName": "John Doe", "mentions": "Credit note for invoice FAC-2026-045 (UPDATED)", "internalNote": "Client cancelled entire order, full refund approved", "lines": [ { "uuid": "910e8400-e29b-41d4-a716-446655440000", "description": "Hosting Services - Annual (CREDIT)", "quantity": -1, "unitPrice": 1200, "unitOfMeasure": "service", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "460e8400-e29b-41d4-a716-446655440000", "discount": 200, "vatIncluded": false }, { "description": "Web Development Services - Phase 1 (CREDIT)", "quantity": -40, "unitPrice": 150, "unitOfMeasure": "hour", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "450e8400-e29b-41d4-a716-446655440000", "vatIncluded": false } ] }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/invoices/850e8400-e29b-41d4-a716-446655440000', { method: 'PUT', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ isCreditNote: true, direction: 'outgoing', parentDocumentId: '650e8400-e29b-41d4-a716-446655440111', clientId: '750e8400-e29b-41d4-a716-446655440000', seriesId: '750e8400-e29b-41d4-a716-446655440000', issueDate: '2026-02-20', dueDate: '2026-03-20', currency: 'RON', notes: 'Updated: Full refund - all services cancelled', paymentTerms: 'Immediate refund', issuerName: 'John Doe', mentions: 'Credit note for invoice FAC-2026-045 (UPDATED)', internalNote: 'Client cancelled entire order, full refund approved', lines: [ { uuid: '910e8400-e29b-41d4-a716-446655440000', description: 'Hosting Services - Annual (CREDIT)', quantity: -1, unitPrice: 1200, unitOfMeasure: 'service', vatRateId: '350e8400-e29b-41d4-a716-446655440000', productId: '460e8400-e29b-41d4-a716-446655440000', discount: 200, vatIncluded: false }, { description: 'Web Development Services - Phase 1 (CREDIT)', quantity: -40, unitPrice: 150, unitOfMeasure: 'hour', vatRateId: '350e8400-e29b-41d4-a716-446655440000', productId: '450e8400-e29b-41d4-a716-446655440000', vatIncluded: false } ] }) }); const data = await response.json(); ``` ## Response Returns the updated credit note object with recalculated totals: ```json { "uuid": "850e8400-e29b-41d4-a716-446655440000", "number": "CN-2026-005", "direction": "outgoing", "isCreditNote": true, "seriesId": "750e8400-e29b-41d4-a716-446655440000", "clientId": "750e8400-e29b-41d4-a716-446655440000", "parentDocumentId": "650e8400-e29b-41d4-a716-446655440111", "status": "draft", "issueDate": "2026-02-20", "dueDate": "2026-03-20", "currency": "RON", "exchangeRate": 1.0, "notes": "Updated: Full refund - all services cancelled", "paymentTerms": "Immediate refund", "issuerName": "John Doe", "mentions": "Credit note for invoice FAC-2026-045 (UPDATED)", "internalNote": "Client cancelled entire order, full refund approved", "lines": [ { "uuid": "910e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Hosting Services - Annual (CREDIT)", "quantity": "-1.00", "unitPrice": "1200.00", "unitOfMeasure": "service", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "460e8400-e29b-41d4-a716-446655440000", "discount": "200.00", "vatIncluded": false, "subtotal": "-1000.00", "vatAmount": "-190.00", "total": "-1190.00" }, { "uuid": "920e8400-e29b-41d4-a716-446655440000", "lineNumber": 2, "description": "Web Development Services - Phase 1 (CREDIT)", "quantity": "-40.00", "unitPrice": "150.00", "unitOfMeasure": "hour", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "450e8400-e29b-41d4-a716-446655440000", "discount": "0.00", "vatIncluded": false, "subtotal": "-6000.00", "vatAmount": "-1140.00", "total": "-7140.00" } ], "subtotal": "-7000.00", "totalDiscount": "200.00", "vatAmount": "-1330.00", "total": "-8330.00", "createdAt": "2026-02-20T09:00:00Z", "updatedAt": "2026-02-20T11:30:00Z" } ``` ## Line Item Behavior When updating lines: - Lines with existing `uuid` values are updated - Lines without `uuid` are created as new lines - Existing lines not included in the request are **deleted** - Line numbers are automatically reassigned sequentially ## Validation Rules ### Status Restriction - Credit note must be in `draft` status - Cannot update after uploading to ANAF - Cannot update validated, uploaded, or error status credit notes ### Parent Invoice Immutability - `parentDocumentId` cannot be changed after creation - Attempting to change it will result in validation error ### Client Match - `clientId` must match parent invoice client - Cannot change to different client ### Currency Match - `currency` must match parent invoice currency - Cannot change currency ### Dates - `issueDate` must be valid YYYY-MM-DD format - `issueDate` should be on or after parent invoice issue date - `dueDate` must be equal to or after `issueDate` ### Amounts - All line totals must be negative - Total credit should not exceed original invoice (warning) ### Line Items - Minimum 1 line required - All line validation rules from create endpoint apply ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid request body structure | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header, or credit note is not editable | | 404 | `not_found` | Credit note not found or doesn't belong to the company | | 409 | `conflict` | Credit note status prevents updates (not in draft) | | 422 | `validation_error` | Validation failed (see error details) | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict ```json { "error": { "code": "conflict", "message": "Credit note cannot be updated", "details": { "status": "uploaded", "reason": "Only draft credit notes can be updated", "uploadedAt": "2026-02-20T10:00:00Z" } } } ``` ### Validation Error ```json { "error": { "code": "validation_error", "message": "Validation failed", "details": { "parentDocumentId": ["Parent document cannot be changed"], "clientId": ["Client must match parent invoice client"], "lines.0.quantity": ["Quantity must be negative for credit notes"], "total": ["Warning: Credit total exceeds original invoice amount"] } } } ``` ## Common Update Scenarios ### Change Credit Amount Update quantities or add/remove line items: ```javascript // Original: -1,190.00 (partial) // Updated: -8,330.00 (full) lines: [ { uuid: "910...", quantity: -1, ... }, // Keep existing { quantity: -40, unitPrice: 150, ... } // Add new line ] ``` ### Update Notes/Reason Clarify the reason for credit: ```javascript notes: "Updated: Full refund approved by management", internalNote: "Client escalated, full refund authorized" ``` ### Correct Line Details Fix description or product reference: ```javascript lines: [{ uuid: "910...", description: "Corrected description", productId: "corrected-product-uuid", // ... other fields }] ``` ### Change Issue Date Adjust the credit note date: ```javascript issueDate: "2026-02-21", // Changed from 2026-02-20 dueDate: "2026-03-21" // Adjust accordingly ``` ## Best Practices 1. **Review before uploading** - Update while in draft, finalize before upload 2. **Track changes** - Log what was changed and why 3. **Verify totals** - Ensure credit doesn't exceed original invoice 4. **Update notes** - Reflect any changes in the notes field 5. **Coordinate with client** - If credit amount changes, notify client 6. **Check parent status** - Ensure parent invoice is still valid 7. **Upload promptly** - Once finalized, upload to ANAF quickly ## When to Update vs Create New ### Update Existing - Credit note is still in draft - Fixing errors before upload - Adjusting amounts before client sees it - Correcting descriptions or references ### Create New Credit Note - Original credit note already uploaded - Need to issue additional credit - Original credit note validated by ANAF - Historical record should be preserved ## Next Steps After updating a credit note: 1. Review all changes carefully 2. Verify totals and calculations 3. Upload to ANAF when ready 4. Generate PDF for client 5. Send to client if amounts changed --- ## Dashboard Statistics > Get comprehensive dashboard statistics and analytics URL: https://docs.storno.ro/api-reference/dashboard/stats # Dashboard Statistics Retrieve comprehensive statistics for the company dashboard including invoice counts, revenue amounts, trends, and activity. --- ## Get Dashboard Stats ```http GET /api/v1/dashboard/stats ``` Get aggregated statistics for the company dashboard. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | | X-Company | string | Yes | Company UUID | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | period | string | No | Predefined period: `month`, `quarter`, `year` | | from | string | No | Custom start date (ISO 8601) | | to | string | No | Custom end date (ISO 8601) | ### Response ```json { "invoiceCounts": { "total": 245, "draft": 12, "issued": 180, "paid": 150, "overdue": 30 }, "amounts": { "totalRevenue": 1250000.50, "totalVat": 237500.10, "totalPaid": 980000.00, "totalUnpaid": 270000.50 }, "monthly": [ { "month": "2026-01", "revenue": 450000.00, "count": 85 }, { "month": "2026-02", "revenue": 380000.50, "count": 72 } ], "topClients": [ { "name": "SC Example SRL", "cif": "12345678", "revenue": 120000.00, "invoiceCount": 15 } ], "topProducts": [ { "name": "Web Development Services", "quantity": 450, "revenue": 225000.00, "invoiceCount": 28 } ], "recentActivity": [ { "type": "invoice_issued", "invoiceNumber": "FINV2026001", "client": "SC Example SRL", "amount": 12500.00, "timestamp": "2026-02-16T10:30:00Z" } ], "paymentSummary": { "onTime": 120, "late": 30, "pending": 45 } } ``` ### Response Fields #### Invoice Counts | Field | Type | Description | |-------|------|-------------| | total | integer | Total number of invoices | | draft | integer | Draft invoices not yet issued | | issued | integer | Invoices issued to ANAF | | paid | integer | Fully paid invoices | | overdue | integer | Overdue unpaid invoices | #### Amounts | Field | Type | Description | |-------|------|-------------| | totalRevenue | number | Total revenue (excluding VAT) | | totalVat | number | Total VAT collected | | totalPaid | number | Total amount received | | totalUnpaid | number | Outstanding receivables | #### Monthly Breakdown | Field | Type | Description | |-------|------|-------------| | month | string | Month in YYYY-MM format | | revenue | number | Revenue for that month | | count | integer | Number of invoices | #### Top Clients | Field | Type | Description | |-------|------|-------------| | name | string | Client company name | | cif | string | Client fiscal code | | revenue | number | Total revenue from client | | invoiceCount | integer | Number of invoices | #### Top Products | Field | Type | Description | |-------|------|-------------| | name | string | Product/service name | | quantity | number | Total quantity sold | | revenue | number | Total revenue generated | | invoiceCount | integer | Number of invoices containing this product | #### Recent Activity | Field | Type | Description | |-------|------|-------------| | type | string | Activity type: `invoice_issued`, `payment_received`, etc. | | invoiceNumber | string | Invoice identifier | | client | string | Client name | | amount | number | Transaction amount | | timestamp | string | ISO 8601 timestamp | #### Payment Summary | Field | Type | Description | |-------|------|-------------| | onTime | integer | Invoices paid on or before due date | | late | integer | Invoices paid after due date | | pending | integer | Unpaid invoices | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - no access to company | | 422 | Invalid query parameters | --- ## Agent Endpoints (Prepare & Result) > Endpoints for local agent-based declaration submission using hardware USB tokens URL: https://docs.storno.ro/api-reference/declarations/agent # Agent Endpoints These endpoints support the **local agent flow** for submitting declarations via hardware USB tokens (SafeNet eToken, Feitian, certSIGN). The private key never leaves the user's device — instead, a local agent on the user's machine proxies the mTLS request to ANAF using `curl` with OS-level certificate access. ## Flow ``` Frontend → GET /prepare → gets XML + ANAF token + URL Frontend → POST https://agent.storno.ro:17394/proxy → local agent uses curl → ANAF SPV Frontend → POST /agent-result → backend processes ANAF response ``` Only `cerere` (submission) and `listaMesaje` (status listing) require mTLS via the agent. `descarcare` (recipisa download) only needs a Bearer token and is handled server-side automatically. ## Local Agent The Storno ANAF Agent runs locally on `https://agent.storno.ro:17394` and provides these endpoints: | Method | Path | Description | |--------|------|-------------| | GET | `/health` | Health check — returns version, platform, and update status | | GET | `/certificates` | List detected client certificates (hardware tokens + OS store) | | POST | `/proxy` | Proxy a single mTLS request to ANAF via curl | | POST | `/batch` | Proxy multiple mTLS requests sequentially | | POST | `/update` | Trigger self-update to the latest version from GitHub releases | ### Auto-Update The `/health` endpoint returns update availability: ```json { "status": "ok", "version": "1.1.0", "platform": "darwin", "update": { "available": true, "latest": "1.2.0", "download": "https://github.com/stornoro/storno/releases/download/agent-v1.2.0/storno-agent-macos-arm64" } } ``` When `update.available` is `true`, the frontend shows a banner. The user can trigger the update via `POST /update`, which downloads the new binary, replaces the current one, and restarts the agent. Download the agent at [get.storno.ro/agent](https://get.storno.ro/agent). --- ## Prepare Declaration `GET /api/v1/declarations/{uuid}/prepare` Prepares a declaration for agent-based submission. Returns the XML content, ANAF URL, Bearer token, and CIF. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ### Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Declaration UUID | ### Query Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `operation` | string | No | `submit` (default) or `listMessages` | ### Request ```bash {% title="cURL" %} curl https://api.storno.ro/api/v1/declarations/a1b2c3d4/prepare \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid" ``` ### Response (operation=submit) ```json { "xml": "...", "anafUrl": "https://webserviced.anaf.ro/SPVWS2/rest/cerere?tip=D394&cui=12345678", "anafToken": "eyJ...", "declarationType": "D394", "cif": "12345678" } ``` ### Response (operation=listMessages) ```json { "anafUrl": "https://webserviced.anaf.ro/SPVWS2/rest/listaMesaje?zile=60", "anafToken": "eyJ...", "declarationType": "D394", "cif": "12345678" } ``` --- ## Agent Result `POST /api/v1/declarations/{uuid}/agent-result` Receives the ANAF response from the local agent after the mTLS-proxied request completes. The server parses the response, extracts the upload ID, sets the declaration status to `processing`, and dispatches asynchronous status checking. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | `application/json` | ### Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Declaration UUID | ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `statusCode` | integer | Yes | HTTP status code from ANAF response | | `headers` | object | No | Response headers from ANAF | | `body` | string | Yes | Response body from ANAF (JSON or XML) | ### Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/declarations/a1b2c3d4/agent-result \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid" \ -H "Content-Type: application/json" \ -d '{"statusCode": 200, "body": "{\"id_solicitare\": \"5000012345\"}"}' ``` ### Response Returns the updated declaration object with status `processing`. ```json { "uuid": "a1b2c3d4", "type": "d394", "status": "processing", "anafUploadId": "5000012345", "metadata": { "uploadResult": {"id_solicitare": "5000012345"}, "submittedViaAgent": true } } ``` ## Error Codes | Status Code | Description | |-------------|-------------| | 400 | Invalid JSON, missing fields, or ANAF returned an error | | 401 | Missing or invalid authentication token | | 403 | Permission denied | | 404 | Declaration not found | --- ## Bulk Submit Declarations > Submit multiple validated declarations to ANAF in a single request URL: https://docs.storno.ro/api-reference/declarations/bulk-submit # Bulk Submit Declarations Submits multiple declarations to ANAF SPV in a single request. Each declaration must be in `validated` status before it can be submitted. Declarations that are not in `validated` status are skipped. Submission is performed asynchronously per declaration. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `ids` | string[] | Yes | Array of declaration UUIDs to submit. Must contain at least one UUID | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/declarations/bulk-submit \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "ids": [ "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "b2c3d4e5-f6a7-8901-bcde-f12345678901", "c3d4e5f6-a7b8-9012-cdef-123456789012" ] }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/declarations/bulk-submit', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: [ 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'b2c3d4e5-f6a7-8901-bcde-f12345678901', 'c3d4e5f6-a7b8-9012-cdef-123456789012' ] }) }); const result = await response.json(); ``` ## Response ```json { "submitted": 3 } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `submitted` | integer | Number of declarations successfully queued for submission. Declarations that were not in `validated` status are excluded from this count | ## Behavior - Only declarations in `validated` status are submitted; others are silently skipped - Each declaration is submitted independently — a failure on one does not block others - After submission, each declaration transitions to `submitted` status and then to `processing` as ANAF acknowledges receipt - Status updates are handled asynchronously; poll individual declarations or use [refresh-statuses](/api-reference/declarations/refresh-statuses) to check progress ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid request body or empty `ids` array | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 422 | `validation_error` | `ids` field is missing or not an array | | 500 | `internal_error` | Server error occurred | --- ## Create Declaration > Create a new tax declaration with data auto-populated from invoices URL: https://docs.storno.ro/api-reference/declarations/create # Create Declaration Creates a new tax declaration in `draft` status. The declaration's `data` field is automatically populated by aggregating invoice data for the specified type and period. The resulting draft can be reviewed, edited, validated, and submitted to ANAF. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `type` | string | Yes | Declaration type: `d394`, `d300`, `d390`, `d100`, `d112` | | `year` | integer | Yes | Fiscal year (e.g., 2026) | | `month` | integer | Yes | Fiscal month (1–12) | | `periodType` | string | No | Period type override (e.g., `monthly`, `quarterly`). Defaults to the standard period for the declaration type | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/declarations \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "type": "d394", "year": 2026, "month": 1 }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/declarations', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'd394', year: 2026, month: 1 }) }); const data = await response.json(); ``` ## Response Returns `201 Created` with the new declaration object. ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "d394", "year": 2026, "month": 1, "periodType": "monthly", "status": "draft", "data": { "totalSalesBase": "84033.61", "totalSalesVat": "15966.39", "totalPurchasesBase": "42016.81", "totalPurchasesVat": "7983.19", "invoiceCount": 47 }, "metadata": { "generatedAt": "2026-02-10T08:00:00Z", "invoiceCountAtGeneration": 47 }, "errorMessage": null, "anafUploadId": null, "xmlPath": null, "recipisaPath": null, "createdAt": "2026-02-10T08:00:00Z", "updatedAt": "2026-02-10T08:00:00Z", "createdBy": "user-uuid-here" } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier of the created declaration | | `type` | string | Declaration type | | `year` | integer | Fiscal year | | `month` | integer | Fiscal month | | `periodType` | string \| null | Resolved period type | | `status` | string | Always `draft` on creation | | `data` | object | Auto-populated fiscal data aggregated from invoices for the period | | `metadata` | object | Generation metadata including timestamp and invoice count snapshot | | `errorMessage` | string \| null | Always `null` on creation | | `anafUploadId` | string \| null | Always `null` on creation | | `xmlPath` | string \| null | Always `null` on creation | | `recipisaPath` | string \| null | Always `null` on creation | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 last-updated timestamp | | `createdBy` | string | UUID of the authenticated user | ## Validation Rules - `type` must be one of: `d394`, `d300`, `d390`, `d100`, `d112` - `year` must be a valid 4-digit year - `month` must be between 1 and 12 - A declaration of the same `type`, `year`, and `month` must not already exist for the company ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid request body structure | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 409 | `conflict` | A declaration for this type and period already exists | | 422 | `validation_error` | Validation failed (e.g., invalid type, invalid month) | | 500 | `internal_error` | Server error occurred | ## Next Steps After creating a declaration: 1. Review and optionally edit the auto-populated data (`PATCH /api/v1/declarations/{uuid}`) 2. Recalculate if invoices were added or changed (`POST /api/v1/declarations/{uuid}/recalculate`) 3. Validate to generate the XML (`POST /api/v1/declarations/{uuid}/validate`) 4. Submit to ANAF (`POST /api/v1/declarations/{uuid}/submit`) --- ## Delete Declaration > Soft-delete a tax declaration (not allowed for accepted declarations) URL: https://docs.storno.ro/api-reference/declarations/delete # Delete Declaration Soft-deletes a tax declaration, removing it from the active list. Accepted declarations cannot be deleted to preserve the fiscal audit trail. Declarations in all other statuses (`draft`, `validated`, `submitted`, `processing`, `rejected`, `error`) may be deleted. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the declaration to delete | ## Request ```bash {% title="cURL" %} curl -X DELETE https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890', { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); // Success: 204 No Content (no response body) if (response.status === 204) { console.log('Declaration deleted successfully'); } ``` ## Response **Success:** Returns `204 No Content` with an empty response body. ## Restrictions Declarations in `accepted` status **cannot** be deleted. Accepted declarations represent filings on record with ANAF and must be retained for audit purposes. All other statuses can be deleted: - `draft` — Not yet submitted; safe to remove - `validated` — XML generated but not submitted - `submitted` / `processing` — In-flight submissions; deletion removes the local record but does not cancel the submission at ANAF - `rejected` / `error` — Failed submissions that are no longer needed ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Declaration not found or doesn't belong to the company | | 409 | `conflict` | Declaration is in `accepted` status and cannot be deleted | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict — Accepted ```json { "error": { "code": "conflict", "message": "Declaration cannot be deleted", "details": { "status": "accepted", "reason": "Accepted declarations cannot be deleted" } } } ``` ### Not Found ```json { "error": { "code": "not_found", "message": "Declaration not found" } } ``` --- ## Download Declaration XML > Download the generated XML file for a declaration URL: https://docs.storno.ro/api-reference/declarations/download-xml # Download Declaration XML Downloads the generated XML file for a declaration. The XML is available once the declaration has been validated (status `validated`, `submitted`, `processing`, `accepted`, `rejected`, or `error`). The file is returned as `application/xml`. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the declaration | ## Request ```bash {% title="cURL" %} curl -X GET https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890/xml \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -o declaration.xml ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890/xml', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); // Save as file in browser const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'declaration.xml'; a.click(); ``` ## Response **Success:** Returns `200 OK` with the XML file as the response body. | Header | Value | |--------|-------| | `Content-Type` | `application/xml` | | `Content-Disposition` | `attachment; filename="d394-2026-01.xml"` | The response body is the raw ANAF-formatted XML document. ```xml RO12345678 012026 ... ... ``` ## Availability The XML file is available when `xmlPath` is set on the declaration object. This is the case for declarations in the following statuses: | Status | XML Available | |--------|--------------| | `draft` | No | | `validated` | Yes | | `submitted` | Yes | | `processing` | Yes | | `accepted` | Yes | | `rejected` | Yes | | `error` | Yes (if XML was generated before the error) | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Declaration not found, doesn't belong to the company, or XML has not been generated yet | | 500 | `internal_error` | Server error occurred | --- ## Download Recipisa > Download the ANAF recipisa PDF for an accepted declaration URL: https://docs.storno.ro/api-reference/declarations/download-recipisa # Download Recipisa Downloads the ANAF recipisa (confirmation receipt) PDF for a declaration that has been accepted by ANAF. The recipisa is the official document confirming that ANAF received and accepted the filing. It is available only when the declaration status is `accepted`. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the declaration | ## Request ```bash {% title="cURL" %} curl -X GET https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890/recipisa \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -o recipisa.pdf ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890/recipisa', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); // Save as file in browser const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'recipisa.pdf'; a.click(); ``` ## Response **Success:** Returns `200 OK` with the PDF file as the response body. | Header | Value | |--------|-------| | `Content-Type` | `application/pdf` | | `Content-Disposition` | `attachment; filename="d394-2026-01-recipisa.pdf"` | The response body is the binary PDF document issued by ANAF confirming acceptance of the declaration. ## Availability The recipisa is available only when `recipisaPath` is set on the declaration object. This occurs when the declaration status is `accepted`. | Status | Recipisa Available | |--------|-------------------| | `draft` | No | | `validated` | No | | `submitted` | No | | `processing` | No | | `accepted` | Yes | | `rejected` | No | | `error` | No | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Declaration not found, doesn't belong to the company, or recipisa is not yet available | | 500 | `internal_error` | Server error occurred | --- ## Get Declaration > Retrieve detailed information for a specific tax declaration URL: https://docs.storno.ro/api-reference/declarations/get # Get Declaration Retrieves the full details of a specific tax declaration, including its fiscal data payload, metadata, and current ANAF submission state. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the declaration to retrieve | ## Request ```bash {% title="cURL" %} curl -X GET https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "d394", "year": 2026, "month": 1, "periodType": "monthly", "status": "accepted", "data": { "totalSalesBase": "84033.61", "totalSalesVat": "15966.39", "totalPurchasesBase": "42016.81", "totalPurchasesVat": "7983.19", "invoiceCount": 47 }, "metadata": { "generatedAt": "2026-02-10T08:05:00Z", "invoiceCountAtGeneration": 47 }, "errorMessage": null, "anafUploadId": "5000012345", "xmlPath": "declarations/2026/01/d394-2026-01.xml", "recipisaPath": "declarations/2026/01/d394-2026-01-recipisa.pdf", "createdAt": "2026-02-10T08:00:00Z", "updatedAt": "2026-02-10T09:15:00Z", "createdBy": "user-uuid-here" } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `type` | string | Declaration type: `d394`, `d300`, `d390`, `d100`, `d112` | | `year` | integer | Fiscal year | | `month` | integer | Fiscal month (1–12) | | `periodType` | string \| null | Period type (e.g., `monthly`, `quarterly`) | | `status` | string | Current status: `draft`, `validated`, `submitted`, `processing`, `accepted`, `rejected`, `error` | | `data` | object | Declaration payload — fiscal figures auto-populated from invoices; structure varies by declaration type | | `metadata` | object | System metadata such as generation timestamp and source invoice counts | | `errorMessage` | string \| null | Error details when status is `rejected` or `error` | | `anafUploadId` | string \| null | ANAF upload index number assigned after submission | | `xmlPath` | string \| null | Internal storage path of the generated XML file | | `recipisaPath` | string \| null | Internal storage path of the ANAF recipisa PDF | | `createdAt` | string | ISO 8601 timestamp of creation | | `updatedAt` | string | ISO 8601 timestamp of last update | | `createdBy` | string \| null | UUID of the user who created the declaration | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Declaration not found or doesn't belong to the company | | 500 | `internal_error` | Server error occurred | --- ## List Declarations > Retrieve a paginated list of tax declarations with optional filtering URL: https://docs.storno.ro/api-reference/declarations/list # List Declarations Retrieves a paginated list of tax declarations for the authenticated company. Declarations cover Romanian fiscal obligations such as D394, D300, D390, D100, and D112 forms. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Query Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `page` | integer | No | Page number (default: 1) | | `limit` | integer | No | Items per page (default: 20, max: 100) | | `type` | string | No | Filter by declaration type: `d394`, `d300`, `d390`, `d100`, `d112` | | `status` | string | No | Filter by status: `draft`, `validated`, `submitted`, `processing`, `accepted`, `rejected`, `error` | | `year` | integer | No | Filter by fiscal year (e.g., 2026) | | `month` | integer | No | Filter by fiscal month (1–12) | ## Request ```bash {% title="cURL" %} curl -X GET "https://api.storno.ro/api/v1/declarations?page=1&limit=20&type=d394&year=2026" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/declarations?page=1&limit=20&type=d394&year=2026', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response ```json { "data": [ { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "d394", "year": 2026, "month": 1, "periodType": "monthly", "status": "accepted", "errorMessage": null, "anafUploadId": "5000012345", "xmlPath": "declarations/2026/01/d394-2026-01.xml", "recipisaPath": "declarations/2026/01/d394-2026-01-recipisa.pdf", "createdAt": "2026-02-10T08:00:00Z", "updatedAt": "2026-02-10T09:15:00Z", "createdBy": "user-uuid-here" } ], "total": 12, "page": 1, "limit": 20, "pages": 1 } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `data` | array | Array of declaration objects | | `total` | integer | Total number of declarations matching the filters | | `page` | integer | Current page number | | `limit` | integer | Items per page | | `pages` | integer | Total number of pages | ### Declaration Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `type` | string | Declaration type: `d394`, `d300`, `d390`, `d100`, `d112` | | `year` | integer | Fiscal year | | `month` | integer | Fiscal month (1–12) | | `periodType` | string \| null | Period type (e.g., `monthly`, `quarterly`) | | `status` | string | Current status: `draft`, `validated`, `submitted`, `processing`, `accepted`, `rejected`, `error` | | `errorMessage` | string \| null | Error details when status is `rejected` or `error` | | `anafUploadId` | string \| null | ANAF upload index number assigned after submission | | `xmlPath` | string \| null | Internal storage path of the generated XML file | | `recipisaPath` | string \| null | Internal storage path of the ANAF recipisa PDF | | `createdAt` | string | ISO 8601 timestamp of creation | | `updatedAt` | string | ISO 8601 timestamp of last update | | `createdBy` | string \| null | UUID of the user who created the declaration | ## Status Lifecycle Declarations follow this status flow: - **draft** — Initial state; data auto-populated from invoices, editable - **validated** — XML generated and schema-validated; ready for submission - **submitted** — Uploaded to ANAF SPV; awaiting processing confirmation - **processing** — ANAF has received and is processing the declaration - **accepted** — ANAF accepted the declaration; recipisa available - **rejected** — ANAF rejected the declaration; check `errorMessage` for details - **error** — A system or communication error occurred during submission ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 422 | `validation_error` | Invalid query parameters (e.g., invalid type or status value) | | 500 | `internal_error` | Server error occurred | --- ## Recalculate Declaration > Recalculate a draft declaration by re-aggregating data from current invoice records URL: https://docs.storno.ro/api-reference/declarations/recalculate # Recalculate Declaration Recalculates the fiscal data of a `draft` declaration by re-aggregating invoice data for the declaration's type and period. Use this endpoint after adding, editing, or deleting invoices in the period to ensure the declaration reflects the latest state. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the declaration to recalculate | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890/recalculate \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890/recalculate', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response Returns the updated declaration object with freshly calculated `data` and updated `metadata`. ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "d394", "year": 2026, "month": 1, "periodType": "monthly", "status": "draft", "data": { "totalSalesBase": "91250.42", "totalSalesVat": "17337.58", "totalPurchasesBase": "44800.00", "totalPurchasesVat": "8512.00", "invoiceCount": 52 }, "metadata": { "generatedAt": "2026-02-10T09:00:00Z", "invoiceCountAtGeneration": 52 }, "errorMessage": null, "anafUploadId": null, "xmlPath": null, "recipisaPath": null, "createdAt": "2026-02-10T08:00:00Z", "updatedAt": "2026-02-10T09:00:00Z", "createdBy": "user-uuid-here" } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `type` | string | Declaration type | | `year` | integer | Fiscal year | | `month` | integer | Fiscal month | | `periodType` | string \| null | Period type | | `status` | string | Always `draft` after recalculation | | `data` | object | Freshly recalculated fiscal data from current invoice records | | `metadata` | object | Updated metadata with new generation timestamp and invoice count | | `errorMessage` | string \| null | Always `null` after successful recalculation | | `xmlPath` | string \| null | Reset to `null` — any previously generated XML is invalidated | | `recipisaPath` | string \| null | `null` until declaration is accepted | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 timestamp of this recalculation | ## Restrictions - The declaration must be in `draft` status - Recalculating always overwrites the current `data` with freshly aggregated values; any manual edits made via PATCH will be replaced ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Declaration not found or doesn't belong to the company | | 409 | `conflict` | Declaration is not in draft status and cannot be recalculated | | 500 | `internal_error` | Server error occurred | --- ## Refresh Declaration Statuses > Check ANAF SPV for status updates on all in-flight declarations URL: https://docs.storno.ro/api-reference/declarations/refresh-statuses # Refresh Declaration Statuses Triggers an asynchronous job that checks ANAF SPV for status updates on all declarations currently in `submitted` or `processing` status. For each in-flight declaration, the job queries ANAF using the stored `anafUploadId` and updates the local status accordingly. If a declaration is accepted, the recipisa is automatically downloaded. Returns `202 Accepted` immediately; status updates are applied in the background. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/declarations/refresh-statuses \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/declarations/refresh-statuses', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); // 202 Accepted — status refresh runs asynchronously ``` ## Response **Success:** Returns `202 Accepted` with an empty response body. The refresh job is queued and runs asynchronously. ## Async Behavior The refresh job processes all declarations in `submitted` or `processing` status for the company. For each declaration: 1. Queries ANAF SPV using the stored `anafUploadId` 2. Maps the ANAF response to the corresponding local status 3. Updates the declaration status in the database 4. If the new status is `accepted`, downloads and stores the recipisa PDF After the job completes, retrieve updated statuses by listing declarations: ``` GET /api/v1/declarations?status=accepted GET /api/v1/declarations?status=rejected ``` ## Status Transitions Applied | ANAF Response | Local Status After Refresh | |---------------|---------------------------| | Processing in progress | `processing` (no change) | | Accepted | `accepted` | | Rejected | `rejected` + `errorMessage` populated | | Not found / expired | `error` + `errorMessage` populated | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 502 | `anaf_error` | Could not connect to ANAF SPV | | 500 | `internal_error` | Server error occurred | --- ## Submit Declaration > Submit a validated declaration to ANAF SPV and begin asynchronous status polling URL: https://docs.storno.ro/api-reference/declarations/submit # Submit Declaration Submits a `validated` declaration to the ANAF SPV (Spatiul Privat Virtual). The XML is uploaded to ANAF, the declaration transitions to `submitted` status, and asynchronous polling begins to track the processing result. When ANAF finishes processing, the status is updated to `accepted`, `rejected`, or `error`. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the declaration to submit | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890/submit \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890/submit', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response Returns the updated declaration object in `submitted` status with the ANAF upload ID assigned. ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "d394", "year": 2026, "month": 1, "periodType": "monthly", "status": "submitted", "data": { "totalSalesBase": "84033.61", "totalSalesVat": "15966.39", "totalPurchasesBase": "42016.81", "totalPurchasesVat": "7983.19", "invoiceCount": 47 }, "metadata": { "generatedAt": "2026-02-10T08:00:00Z", "validatedAt": "2026-02-10T08:10:00Z", "submittedAt": "2026-02-10T08:15:00Z" }, "errorMessage": null, "anafUploadId": "5000012345", "xmlPath": "declarations/2026/01/d394-2026-01.xml", "recipisaPath": null, "createdAt": "2026-02-10T08:00:00Z", "updatedAt": "2026-02-10T08:15:00Z", "createdBy": "user-uuid-here" } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `type` | string | Declaration type | | `year` | integer | Fiscal year | | `month` | integer | Fiscal month | | `periodType` | string \| null | Period type | | `status` | string | `submitted` immediately after upload to ANAF | | `data` | object | The fiscal data included in the submission | | `metadata` | object | Updated metadata including `submittedAt` timestamp | | `errorMessage` | string \| null | `null` on successful upload; may be populated if ANAF communication fails | | `anafUploadId` | string | ANAF-assigned upload index number for tracking the submission | | `xmlPath` | string | Internal storage path of the submitted XML file | | `recipisaPath` | string \| null | `null` until ANAF accepts the declaration | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 timestamp of this submission | ## Restrictions - The declaration must be in `validated` status - The company must have valid ANAF SPV credentials configured - ANAF processing is asynchronous — the final `accepted`/`rejected` status is determined by polling ## Status After Submission After calling this endpoint, the declaration progresses through these states automatically: 1. `submitted` — XML uploaded to ANAF SPV; `anafUploadId` assigned 2. `processing` — ANAF has acknowledged receipt and is processing 3. `accepted` — ANAF accepted the filing; recipisa downloaded and stored 4. `rejected` — ANAF rejected the filing; `errorMessage` contains the reason 5. `error` — A communication or system error interrupted status tracking Use [refresh-statuses](/api-reference/declarations/refresh-statuses) to force an immediate status check, or poll [GET /api/v1/declarations/{uuid}](/api-reference/declarations/get) periodically. ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Declaration not found or doesn't belong to the company | | 409 | `conflict` | Declaration is not in validated status | | 502 | `anaf_error` | ANAF SPV returned an error or was unreachable | | 500 | `internal_error` | Server error occurred | --- ## Sync Declarations from ANAF > Discover and import declarations filed with ANAF for a given year URL: https://docs.storno.ro/api-reference/declarations/sync # Sync Declarations from ANAF Triggers an asynchronous sync that discovers declarations filed with ANAF for a given fiscal year via SPV messages. For each filing found, if no local record exists it is created and the corresponding recipisa is downloaded. Useful for importing historical filings or reconciling records after an ANAF SPV migration. Returns `202 Accepted` immediately; the actual sync runs in the background. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `year` | integer | Yes | Fiscal year to sync (e.g., 2025) | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/declarations/sync \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "year": 2025 }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/declarations/sync', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ year: 2025 }) }); // 202 Accepted — sync runs asynchronously ``` ## Response **Success:** Returns `202 Accepted` with an empty response body. The sync job is queued and runs asynchronously. ## Async Behavior The sync job performs the following steps in the background: 1. Queries ANAF SPV messages for the specified year to find submission receipts 2. For each filing discovered, checks whether a local declaration record already exists 3. Creates missing records with status `accepted` and populates available metadata 4. Downloads the recipisa PDF from ANAF for each newly created record To check the results, list declarations filtered by year after the sync completes: ``` GET /api/v1/declarations?year=2025&status=accepted ``` ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid request body or missing `year` field | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 422 | `validation_error` | `year` is not a valid 4-digit year | | 502 | `anaf_error` | Could not connect to ANAF SPV to initiate sync | | 500 | `internal_error` | Server error occurred | --- ## Update Declaration > Update the data or metadata of a draft tax declaration URL: https://docs.storno.ro/api-reference/declarations/update # Update Declaration Partially updates a tax declaration's `data` or `metadata` fields. Only declarations in `draft` status can be updated. Once a declaration has been validated or submitted, it becomes immutable. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the declaration to update | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `data` | object | No | Partial or full replacement of the declaration's fiscal data payload | | `metadata` | object | No | Partial or full replacement of the declaration's metadata | At least one of `data` or `metadata` must be provided. ## Request ```bash {% title="cURL" %} curl -X PATCH https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "data": { "totalSalesBase": "85000.00", "totalSalesVat": "16150.00", "totalPurchasesBase": "42016.81", "totalPurchasesVat": "7983.19", "invoiceCount": 48 } }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890', { method: 'PATCH', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ data: { totalSalesBase: '85000.00', totalSalesVat: '16150.00', totalPurchasesBase: '42016.81', totalPurchasesVat: '7983.19', invoiceCount: 48 } }) }); const declaration = await response.json(); ``` ## Response Returns the updated declaration object. ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "d394", "year": 2026, "month": 1, "periodType": "monthly", "status": "draft", "data": { "totalSalesBase": "85000.00", "totalSalesVat": "16150.00", "totalPurchasesBase": "42016.81", "totalPurchasesVat": "7983.19", "invoiceCount": 48 }, "metadata": { "generatedAt": "2026-02-10T08:00:00Z", "invoiceCountAtGeneration": 47 }, "errorMessage": null, "anafUploadId": null, "xmlPath": null, "recipisaPath": null, "createdAt": "2026-02-10T08:00:00Z", "updatedAt": "2026-02-10T08:30:00Z", "createdBy": "user-uuid-here" } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `type` | string | Declaration type | | `year` | integer | Fiscal year | | `month` | integer | Fiscal month | | `periodType` | string \| null | Period type | | `status` | string | Always `draft` after a successful update | | `data` | object | Updated fiscal data payload | | `metadata` | object | Updated metadata | | `errorMessage` | string \| null | Error details (null for draft declarations) | | `anafUploadId` | string \| null | Always `null` for draft declarations | | `xmlPath` | string \| null | Path to generated XML (null until validated) | | `recipisaPath` | string \| null | Path to ANAF recipisa (null until accepted) | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 timestamp of this update | | `createdBy` | string \| null | UUID of the user who created the declaration | ## Restrictions - The declaration must be in `draft` status - `type`, `year`, `month`, and `periodType` cannot be changed via this endpoint - To refresh data from invoices automatically, use the [recalculate endpoint](/api-reference/declarations/recalculate) instead ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid request body structure | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Declaration not found or doesn't belong to the company | | 409 | `conflict` | Declaration is not in draft status and cannot be updated | | 422 | `validation_error` | Validation failed | | 500 | `internal_error` | Server error occurred | ## Example Error Response ### Status Conflict ```json { "error": { "code": "conflict", "message": "Declaration cannot be updated", "details": { "status": "validated", "reason": "Only draft declarations can be updated" } } } ``` --- ## Upload Declaration XML > Upload an XML file to create a declaration by parsing type and period from its contents URL: https://docs.storno.ro/api-reference/declarations/upload # Upload Declaration XML Creates a new tax declaration by uploading an existing XML file. The server parses the XML to automatically extract the declaration type, fiscal year, and period. This is useful for importing declarations that were generated externally or by other accounting software. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `multipart/form-data` | ## Request Body Multipart form data. | Field | Type | Required | Description | |-------|------|----------|-------------| | `file` | file | Yes | The XML declaration file to upload. Must be a valid ANAF-formatted XML document | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/declarations/upload \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -F "file=@/path/to/D394_2026_01.xml" ``` ```javascript {% title="JavaScript" %} const formData = new FormData(); formData.append('file', xmlFileBlob, 'D394_2026_01.xml'); const response = await fetch('https://api.storno.ro/api/v1/declarations/upload', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' }, body: formData }); const data = await response.json(); ``` ## Response Returns `201 Created` with the new declaration object. The declaration is created in `draft` status with `data` parsed from the uploaded XML. ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "d394", "year": 2026, "month": 1, "periodType": "monthly", "status": "draft", "data": { "totalSalesBase": "84033.61", "totalSalesVat": "15966.39", "totalPurchasesBase": "42016.81", "totalPurchasesVat": "7983.19", "invoiceCount": 47 }, "metadata": { "source": "upload", "originalFilename": "D394_2026_01.xml", "uploadedAt": "2026-02-10T08:00:00Z" }, "errorMessage": null, "anafUploadId": null, "xmlPath": "declarations/2026/01/d394-2026-01-uploaded.xml", "recipisaPath": null, "createdAt": "2026-02-10T08:00:00Z", "updatedAt": "2026-02-10T08:00:00Z", "createdBy": "user-uuid-here" } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier of the created declaration | | `type` | string | Declaration type parsed from the XML | | `year` | integer | Fiscal year parsed from the XML | | `month` | integer | Fiscal month parsed from the XML | | `periodType` | string \| null | Period type parsed from the XML | | `status` | string | Always `draft` after upload | | `data` | object | Fiscal data parsed from the uploaded XML | | `metadata` | object | Upload metadata including original filename and upload timestamp | | `xmlPath` | string | Internal storage path where the uploaded XML file is stored | | `recipisaPath` | string \| null | Always `null` after upload | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 last-updated timestamp | | `createdBy` | string | UUID of the authenticated user | ## Validation Rules - The uploaded file must be a valid XML document - The XML must contain recognizable ANAF declaration structure with a parseable type and period - A declaration of the same type, year, and month must not already exist for the company ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | No file provided or file is not valid XML | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 409 | `conflict` | A declaration for the parsed type and period already exists | | 422 | `validation_error` | XML does not contain a recognizable declaration type or period | | 500 | `internal_error` | Server error occurred | --- ## Validate Declaration > Validate a draft declaration by generating and schema-checking its XML URL: https://docs.storno.ro/api-reference/declarations/validate # Validate Declaration Validates a `draft` declaration by generating the ANAF-formatted XML and running it through schema validation. If validation passes, the declaration transitions to `validated` status and is ready for submission. Any schema errors are returned as a validation error response. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the declaration to validate | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890/validate \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/declarations/a1b2c3d4-e5f6-7890-abcd-ef1234567890/validate', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response Returns the updated declaration object in `validated` status with the generated XML path populated. ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "d394", "year": 2026, "month": 1, "periodType": "monthly", "status": "validated", "data": { "totalSalesBase": "84033.61", "totalSalesVat": "15966.39", "totalPurchasesBase": "42016.81", "totalPurchasesVat": "7983.19", "invoiceCount": 47 }, "metadata": { "generatedAt": "2026-02-10T08:00:00Z", "invoiceCountAtGeneration": 47, "validatedAt": "2026-02-10T08:10:00Z" }, "errorMessage": null, "anafUploadId": null, "xmlPath": "declarations/2026/01/d394-2026-01.xml", "recipisaPath": null, "createdAt": "2026-02-10T08:00:00Z", "updatedAt": "2026-02-10T08:10:00Z", "createdBy": "user-uuid-here" } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `type` | string | Declaration type | | `year` | integer | Fiscal year | | `month` | integer | Fiscal month | | `periodType` | string \| null | Period type | | `status` | string | `validated` after successful validation | | `data` | object | The fiscal data used to generate the XML | | `metadata` | object | Updated metadata including `validatedAt` timestamp | | `errorMessage` | string \| null | `null` on success; contains XML schema errors on validation failure | | `xmlPath` | string | Internal storage path of the generated XML file | | `recipisaPath` | string \| null | `null` until declaration is accepted | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 timestamp of this validation | ## Restrictions - The declaration must be in `draft` status - The company profile must have all required fiscal data (CIF, address, etc.) to generate a valid XML ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Declaration not found or doesn't belong to the company | | 409 | `conflict` | Declaration is not in draft status | | 422 | `validation_error` | Generated XML failed schema validation; details contain specific errors | | 500 | `internal_error` | Server error occurred | ## Example Validation Error Response ```json { "error": { "code": "validation_error", "message": "XML schema validation failed", "details": { "schemaErrors": [ "Element 'CodFiscal': This element is not expected. Expected is one of ( NrInreg, DataInreg ).", "Element 'TotalBaza': '0' is not a valid value of the atomic type 'pozitiv'." ] } } } ``` ## Next Steps After a successful validation: 1. Download the generated XML for review (`GET /api/v1/declarations/{uuid}/xml`) 2. Submit to ANAF (`POST /api/v1/declarations/{uuid}/submit`) --- ## Invoice Defaults > Get default values and configuration for invoices URL: https://docs.storno.ro/api-reference/defaults/invoice-defaults # Invoice Defaults Retrieve default values and dropdown options for invoice creation, including VAT rates, currencies, payment terms, exchange rates, and client-specific VAT rule indicators. --- ## Get Invoice Defaults ```http GET /api/v1/invoice-defaults?clientId={clientId} ``` Get all default values and configuration options for invoice creation. This endpoint provides all dropdown options and current exchange rates needed by invoice forms. When a `clientId` is provided, it also returns client-specific VAT rule indicators (reverse charge, OSS, export). ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | | X-Company | string | Yes | Company UUID | ### Query Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | clientId | string | No | Client UUID. When provided, returns client-specific VAT flags: `reverseCharge`, `ossApplicable`, `exportApplicable`. | ### Response ```json { "vatRates": [ { "rate": "21", "label": "Standard 21%", "categoryCode": "S", "default": true }, { "rate": "9", "label": "Redus 9%", "categoryCode": "S", "default": false }, { "rate": "5", "label": "Redus 5%", "categoryCode": "S", "default": false }, { "rate": "0", "label": "Scutit", "categoryCode": "Z", "default": false } ], "currencies": ["RON", "EUR", "USD", "GBP", "CHF", "HUF", "CZK", "PLN", "BGN", "SEK", "NOK", "DKK"], "defaultCurrency": "RON", "defaultPaymentTermDays": 30, "defaultUnitOfMeasure": "buc", "unitsOfMeasure": [ { "value": "buc", "label": "buc (Bucata)", "code": "H87" }, { "value": "ora", "label": "ora (Ora)", "code": "HUR" } ], "documentSeriesTypes": [ { "value": "invoice", "label": "Factura" }, { "value": "proforma", "label": "Proforma" }, { "value": "credit_note", "label": "Nota de credit" }, { "value": "delivery_note", "label": "Aviz de insotire" } ], "paymentMethods": [ { "value": "bank_transfer", "label": "Transfer bancar" }, { "value": "cash", "label": "Numerar" }, { "value": "card", "label": "Card" }, { "value": "cheque", "label": "Cec / Bilet la ordin" }, { "value": "other", "label": "Altele" } ], "exchangeRates": { "EUR": 4.975, "USD": 4.56 }, "exchangeRateDate": "2026-03-01", "isVatPayer": true, "reverseCharge": false, "exportApplicable": false, "ossApplicable": false, "ossVatRate": null, "ossVatRates": [], "countries": [ { "code": "RO", "label": "Romania" }, { "code": "DE", "label": "Germania" } ], "counties": [ { "code": "B", "label": "Bucuresti" }, { "code": "CJ", "label": "Cluj" } ] } ``` ### Response Fields #### VAT Rates | Field | Type | Description | |-------|------|-------------| | rate | string | VAT rate percentage (e.g., "21", "9", "0") | | label | string | Display label | | categoryCode | string | UBL VAT category code: `S` (standard), `Z` (zero), `AE` (reverse charge), `E` (exempt) | | default | boolean | Whether this is the pre-selected rate. When `reverseCharge` or `exportApplicable` is true, the 0% rate is default. | #### Client-Specific VAT Flags | Field | Type | Description | |-------|------|-------------| | isVatPayer | boolean | Whether the company is a VAT payer | | reverseCharge | boolean | `true` when the client is an EU company with a valid VIES VAT number. A "Taxare inversă / Reverse Charge" rate (0%, category AE) is prepended to vatRates. | | exportApplicable | boolean | `true` when the client is in a non-EU country. The 0% rate becomes the default. Use `autoApplyVatRules` on invoice creation to automatically set lines to 0% VAT with category Z. | | ossApplicable | boolean | `true` when the client is in an EU country (not RO), the company has OSS enabled, and the client is not VIES-valid. Destination country VAT rates are returned in `ossVatRates`. | | ossVatRate | object\|null | The standard OSS rate for the destination country (backwards compatibility). Contains `rate`, `label`, `categoryCode`. | | ossVatRates | array | All available OSS VAT rates for the destination country (standard, reduced, etc.). Each has `rate`, `label`, `categoryCode`, `default`. | #### Currencies Array of ISO 4217 currency codes (e.g., `["RON", "EUR", "USD", ...]`). #### Document Series Types | Field | Type | Description | |-------|------|-------------| | value | string | Series type identifier: `invoice`, `proforma`, `credit_note`, `delivery_note` | | label | string | Display label in Romanian | #### Payment Methods | Field | Type | Description | |-------|------|-------------| | value | string | Method identifier: `bank_transfer`, `cash`, `card`, `cheque`, `other` | | label | string | Method name in Romanian | #### Units of Measure | Field | Type | Description | |-------|------|-------------| | value | string | Unit code (e.g., `buc`, `ora`, `kg`) | | label | string | Display label with full name | | code | string | UBL/UN/ECE unit code (e.g., `H87`, `HUR`, `KGM`) | #### Exchange Rates | Field | Type | Description | |-------|------|-------------| | exchangeRates | object | Key-value pairs: currency code → rate to RON. Updated daily from BNR. | | exchangeRateDate | string\|null | Date of the exchange rates (ISO 8601: YYYY-MM-DD) | #### Countries & Counties | Field | Type | Description | |-------|------|-------------| | countries | array | ISO 3166-1 alpha-2 country codes with Romanian labels | | counties | array | Romanian county codes with labels (for `countrySubentity` field) | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - no access to company | ### Notes - VAT rates are loaded from the company's configured rates (not hardcoded) - When the company is not a VAT payer, the 0% rate becomes default - Exchange rates reflect the latest BNR (Banca Națională a României) rates - Frontend and mobile apps MUST NOT hardcode these values - Use this endpoint to populate invoice form dropdowns - Pass `clientId` to get VAT rule indicators before creating an invoice --- ## Bulk Convert Delivery Notes > Convert multiple issued delivery notes into a single invoice URL: https://docs.storno.ro/api-reference/delivery-notes/bulk-convert # Bulk Convert Delivery Notes Converts multiple issued delivery notes into a single consolidated invoice. All line items from every delivery note are combined into one invoice. Each delivery note is then marked as `converted`. This is useful for end-of-period billing where multiple deliveries to the same client should appear on one invoice. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `ids` | string[] | Yes | Array of delivery note UUIDs to convert (minimum 2) | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/delivery-notes/bulk-convert \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "ids": [ "950e8400-e29b-41d4-a716-446655440000", "951e8400-e29b-41d4-a716-446655440001", "952e8400-e29b-41d4-a716-446655440002" ] }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/bulk-convert', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: [ '950e8400-e29b-41d4-a716-446655440000', '951e8400-e29b-41d4-a716-446655440001', '952e8400-e29b-41d4-a716-446655440002' ] }) }); const invoice = await response.json(); ``` ## Response Returns the newly created invoice object in `draft` status: ```json { "uuid": "650e8400-e29b-41d4-a716-446655440222", "number": "FAC-2026-051", "status": "draft", "direction": "outgoing", "isCreditNote": false, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "email": "contact@client.ro" }, "currency": "RON", "exchangeRate": 1.0, "issueDate": "2026-02-28", "dueDate": "2026-03-28", "lines": [ { "uuid": "C10e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Laptop Dell Latitude 7420", "quantity": "10.00", "unitPrice": "450.00", "unitOfMeasure": "piece", "subtotal": "4500.00", "vatAmount": "855.00", "total": "5355.00" }, { "uuid": "C20e8400-e29b-41d4-a716-446655440000", "lineNumber": 2, "description": "Wireless Mouse Logitech MX Master 3", "quantity": "5.00", "unitPrice": "50.00", "unitOfMeasure": "piece", "subtotal": "250.00", "vatAmount": "47.50", "total": "297.50" } ], "subtotal": "4750.00", "vatAmount": "902.50", "total": "5652.50", "deliveryNoteIds": [ "950e8400-e29b-41d4-a716-446655440000", "951e8400-e29b-41d4-a716-446655440001", "952e8400-e29b-41d4-a716-446655440002" ], "createdAt": "2026-02-28T16:00:00Z", "updatedAt": "2026-02-28T16:00:00Z" } ``` ## State Changes ### Created Invoice - New invoice created in `draft` status - Lines from all delivery notes are merged in order - `deliveryNoteIds` lists all source delivery note UUIDs - Issue date defaults to today - The company's default `invoice` series is automatically assigned to the created invoice ### Source Delivery Notes - Each delivery note status is changed from `issued` → `converted` - `convertedAt` is set on each delivery note - `convertedInvoiceId` is set to the new invoice UUID ## Validation Rules All delivery notes in the `ids` array must: - Exist and belong to the same company - Be in `issued` status - Share the same client - Share the same currency | Validation Failure | Error Code | Description | |--------------------|------------|-------------| | Different clients | `validation_error` | All delivery notes must have the same client | | Different currencies | `validation_error` | All delivery notes must use the same currency | | Non-issued status | `validation_error` | All delivery notes must be in issued status | | Not found | `not_found` | One or more delivery note UUIDs not found | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | One or more delivery notes not found or not in this company | | 422 | `validation_error` | Validation failed (different clients, currencies, or non-issued status) | | 500 | `internal_error` | Server error occurred | ## Example Error Response ### Mixed Clients ```json { "error": { "code": "validation_error", "message": "Cannot bulk convert delivery notes", "details": { "reason": "All delivery notes must belong to the same client", "conflictingIds": [ "952e8400-e29b-41d4-a716-446655440002" ] } } } ``` ## Workflow Integration ### End-of-Period Billing Flow 1. Issue delivery notes throughout the month 2. At period end, collect all issued delivery note UUIDs for a client 3. **Bulk convert** (`POST /api/v1/delivery-notes/bulk-convert`) ← You are here 4. Review the consolidated invoice 5. Upload to ANAF (`POST /api/v1/invoices/{uuid}/upload`) 6. Send invoice to client ## Related Endpoints - [Convert single delivery note](/api-reference/delivery-notes/convert) - Convert one delivery note to invoice - [List delivery notes](/api-reference/delivery-notes/list) - Find issued delivery notes to bulk convert - [Get invoice](/api-reference/invoices/get) - View the created invoice --- ## Cancel Delivery Note > Cancel a delivery note when delivery will not occur URL: https://docs.storno.ro/api-reference/delivery-notes/cancel # Cancel Delivery Note Cancels a delivery note by transitioning it to `cancelled` status. This action is used when a planned delivery will not occur, or when a delivery needs to be invalidated for any reason. Unlike deletion, cancellation preserves the delivery note for historical records and audit trail while preventing any further actions. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the delivery note to cancel | ## Request Body (Optional) | Field | Type | Required | Description | |-------|------|----------|-------------| | `cancellationReason` | string | No | Reason for cancellation | | `cancellationNotes` | string | No | Internal notes about the cancellation | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/cancel \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "cancellationReason": "Client cancelled order", "cancellationNotes": "Client no longer needs equipment, full order cancelled" }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/cancel', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ cancellationReason: 'Client cancelled order', cancellationNotes: 'Client no longer needs equipment, full order cancelled' }) }); const data = await response.json(); ``` ## Response Returns the updated delivery note with `status = cancelled` and `cancelledAt` timestamp: ```json { "uuid": "950e8400-e29b-41d4-a716-446655440000", "number": "DN-2026-012", "seriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "DN", "nextNumber": 13 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "email": "contact@client.ro" }, "status": "cancelled", "issueDate": "2026-02-18", "dueDate": "2026-03-18", "currency": "RON", "exchangeRate": 1.0, "subtotal": "5000.00", "vatAmount": "950.00", "total": "5950.00", "deliveryLocation": "Client warehouse - Str. Depozit 5, București", "projectReference": "PROJECT-2026-002", "deputyName": "Maria Ionescu", "notes": "Handle with care - fragile items", "cancellationReason": "Client cancelled order", "cancellationNotes": "Client no longer needs equipment, full order cancelled", "issuedAt": "2026-02-18T14:30:00Z", "cancelledAt": "2026-02-19T09:15:00Z", "convertedAt": null, "convertedInvoiceId": null, "createdAt": "2026-02-18T09:00:00Z", "updatedAt": "2026-02-19T09:15:00Z" } ``` ## State Changes ### Status Transition - **Before:** Any status except `converted` or `cancelled` - **After:** `status = cancelled` ### Timestamp - Sets `cancelledAt` to current UTC timestamp (ISO 8601 format) - Updates `updatedAt` timestamp ### Restrictions Applied Once cancelled: - Cannot be converted to invoice - Cannot be issued (if was in draft) - Cannot be edited or deleted - Serves as historical record only ## Validation Rules ### Status Requirement Can cancel delivery note in these statuses: - `draft` - Not yet issued - `issued` - Issued but not yet converted Cannot cancel delivery note in these statuses: - `converted` - Already converted to invoice (use credit note instead) - `cancelled` - Already cancelled ### Business Logic Cancelling a delivery note indicates: - Delivery will not occur - Order was cancelled - Goods not available - Error in delivery planning - Client requested cancellation ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Delivery note not found or doesn't belong to the company | | 409 | `conflict` | Delivery note status prevents cancellation (already converted or cancelled) | | 422 | `validation_error` | Invalid request body (if cancellation reason/notes provided) | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict - Already Cancelled ```json { "error": { "code": "conflict", "message": "Delivery note cannot be cancelled", "details": { "status": "cancelled", "reason": "Delivery note is already cancelled", "cancelledAt": "2026-02-19T09:15:00Z" } } } ``` ### Status Conflict - Already Converted ```json { "error": { "code": "conflict", "message": "Delivery note cannot be cancelled", "details": { "status": "converted", "reason": "Delivery note has already been converted to an invoice. Use credit note to reverse the invoice instead.", "convertedAt": "2026-02-20T10:00:00Z", "convertedInvoiceId": "650e8400-e29b-41d4-a716-446655440222", "convertedInvoiceNumber": "FAC-2026-050" } } } ``` ## Cancellation Reasons ### Common Reasons - **Client cancelled order** - Client no longer needs delivery - **Out of stock** - Items not available for delivery - **Transport issues** - Cannot complete delivery due to logistics - **Weather conditions** - Delivery impossible due to weather - **Wrong items prepared** - Incorrect items staged for delivery - **Client unavailable** - Cannot receive delivery at scheduled time - **Payment issues** - Client payment problems - **Duplicate delivery note** - Created by mistake - **Address issues** - Cannot locate delivery address - **Order changed** - Requirements changed significantly ## Cancel vs Delete ### Cancel - **Use when:** Delivery was planned but won't occur - **Preserves:** Full audit trail and historical data - **Status:** `cancelled` - **Can be done:** At any status (except converted/cancelled) - **Inventory:** Can be returned to available stock ### Delete - **Use when:** Delivery note created by mistake (draft only) - **Preserves:** Nothing - permanently removed - **Status:** N/A - record is deleted - **Can be done:** Only in `draft` status - **Inventory:** No inventory impact (wasn't committed) ## Workflow Integration ### Cancellation Flow 1. Determine cancellation is necessary 2. **Call cancel endpoint** (`POST /api/v1/delivery-notes/{uuid}/cancel`) ← You are here 3. Update inventory systems (return stock to available) 4. Notify relevant stakeholders 5. Cancel related transport orders 6. Update CRM and project management systems ### Notification Strategy After cancellation: - Notify logistics team - Update warehouse/inventory system - Cancel transport booking - Inform client if they're expecting delivery - Log reason in CRM - Update project status ## Inventory Implications When cancelling a delivery note: ### For Draft Delivery Notes - Items were reserved but not yet committed - Return reserved items to available stock - No physical movement occurred ### For Issued Delivery Notes - If goods already shipped, arrange return - If goods in transit, coordinate with logistics - Update warehouse records - Restock returned items ## Integration Points ### Warehouse Management - Return reserved inventory to available - Cancel pick list - Update stock locations - Recount if necessary ### Logistics Systems - Cancel transport order - Notify driver if in transit - Update route planning - Close shipment record ### ERP/Accounting - Update order status - Cancel related purchase orders - Update financial projections - Record cancellation for reporting ## Best Practices 1. **Always provide reason** - Essential for analytics and audit 2. **Coordinate returns** - If goods already shipped 3. **Update inventory** - Restore stock levels immediately 4. **Communicate promptly** - Notify all affected parties 5. **Track patterns** - Monitor cancellation reasons to improve processes 6. **Document thoroughly** - Use internal notes for detailed context 7. **Follow up** - If due to temporary issue, reschedule delivery 8. **Update systems** - Sync status to all integrated systems ## Analytics and Reporting ### Cancellation Metrics Track these metrics for process improvement: - Cancellation rate by status (draft vs issued) - Time between creation and cancellation - Most common cancellation reasons - Cancellations by client or product - Cost of cancelled deliveries ### Process Improvement Use cancellation data to: - Identify unreliable clients - Improve inventory planning - Optimize delivery scheduling - Reduce out-of-stock situations - Better qualify orders before preparation ## Recovery After Cancellation Common next steps after cancellation: ### Undo Accidental Cancellation If the delivery note was cancelled by mistake, use the `POST /api/v1/delivery-notes/{uuid}/restore` endpoint to restore it back to `draft` status. The delivery note can then be edited and re-issued. ### Temporary Cancellation 1. **Reschedule delivery** - Create new delivery note for later date 2. **Reserve inventory** - Keep items reserved for rescheduled delivery 3. **Coordinate with client** - Agree on new delivery date ### Permanent Cancellation 1. **Return to stock** - Make items available for other orders 2. **Cancel order** - Close the entire order in system 3. **Update forecast** - Adjust sales projections 4. **Learn from it** - Document reason for future reference ### Partial Cancellation 1. **Create new delivery note** - With reduced quantities 2. **Return excess** - Restock items not needed 3. **Adjust order** - Update order to reflect actual delivery ## Special Scenarios ### Goods Already in Transit ``` 1. Cancel delivery note in system 2. Contact driver/logistics immediately 3. Arrange return to warehouse 4. Update inventory on return 5. Create return documentation ``` ### Partial Delivery Completed ``` 1. Cannot cancel converted portion 2. May need to issue credit note for delivered portion 3. Cancel remaining undelivered portion 4. Create separate delivery note for future delivery if needed ``` ### Cross-Border Shipment ``` 1. Cancel delivery note 2. Handle customs documentation 3. Arrange return shipment if needed 4. Update international logistics partners 5. Handle any customs duties/fees ``` ## Compliance Notes ### Documentation Requirements - Keep cancelled delivery notes for audit period - Document cancellation reason clearly - Retain communication with client - Track inventory movements ### Tax Implications - Cancelled delivery notes don't generate revenue - No VAT implications if not converted - May affect period-end reporting - Consider inventory valuation impacts ## After Cancellation Next steps after cancelling: 1. Verify inventory updated correctly 2. Confirm transport cancellation 3. Update client records 4. Close related tasks/tickets 5. Create new delivery note if rescheduling 6. Document lessons learned --- ## Convert Delivery Note to Invoice > Convert a delivery note into an invoice for payment URL: https://docs.storno.ro/api-reference/delivery-notes/convert # Convert Delivery Note to Invoice Converts a delivery note into a final, legally-binding invoice. This action creates a new invoice with all the delivery note's data, marks the delivery note as `converted`, and establishes a link between the two documents. Once converted, the delivery note cannot be modified and serves as a historical reference to the original delivery. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the delivery note to convert | ## Request Body (Optional) | Field | Type | Required | Description | |-------|------|----------|-------------| | `invoiceSeriesId` | string | No | UUID of invoice series (if different from delivery note series) | | `issueDate` | string | No | Override issue date (default: today) | | `dueDate` | string | No | Override due date (default: delivery note's due date) | | `overrideFields` | object | No | Fields to override from the delivery note data | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/convert \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "invoiceSeriesId": "660e8400-e29b-41d4-a716-446655440000", "issueDate": "2026-02-20", "dueDate": "2026-03-20" }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/convert', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ invoiceSeriesId: '660e8400-e29b-41d4-a716-446655440000', issueDate: '2026-02-20', dueDate: '2026-03-20' }) }); const data = await response.json(); ``` ## Response Returns the newly created invoice object along with updated delivery note status: ```json { "invoice": { "uuid": "650e8400-e29b-41d4-a716-446655440222", "number": "FAC-2026-050", "direction": "outgoing", "isCreditNote": false, "seriesId": "660e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "660e8400-e29b-41d4-a716-446655440000", "name": "FAC", "nextNumber": 51, "prefix": "FAC-", "year": 2026 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "address": "Str. Exemplu 123, București", "email": "contact@client.ro", "phone": "+40721234567" }, "status": "draft", "issueDate": "2026-02-20", "dueDate": "2026-03-20", "currency": "RON", "exchangeRate": 1.0, "invoiceTypeCode": "380", "deliveryLocation": "Client warehouse - Str. Depozit 5, București", "projectReference": "PROJECT-2026-002", "deliveryNoteReference": "DN-2026-012", "deliveryNoteId": "950e8400-e29b-41d4-a716-446655440000", "notes": "Handle with care - fragile items", "issuerName": "John Doe", "lines": [ { "uuid": "A40e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Laptop Dell Latitude 7420", "quantity": "10.00", "unitPrice": "450.00", "unitOfMeasure": "piece", "productId": "B50e8400-e29b-41d4-a716-446655440000", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "percentage": "19.00" }, "subtotal": "4500.00", "vatAmount": "855.00", "total": "5355.00" }, { "uuid": "A50e8400-e29b-41d4-a716-446655440000", "lineNumber": 2, "description": "Wireless Mouse Logitech MX Master 3", "quantity": "10.00", "unitPrice": "50.00", "unitOfMeasure": "piece", "productId": "B60e8400-e29b-41d4-a716-446655440000", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "percentage": "19.00" }, "subtotal": "500.00", "vatAmount": "95.00", "total": "595.00" } ], "subtotal": "5000.00", "vatAmount": "950.00", "total": "5950.00", "anafStatus": null, "anafUploadIndex": null, "createdAt": "2026-02-20T15:00:00Z", "updatedAt": "2026-02-20T15:00:00Z" }, "deliveryNote": { "uuid": "950e8400-e29b-41d4-a716-446655440000", "number": "DN-2026-012", "status": "converted", "convertedAt": "2026-02-20T15:00:00Z", "convertedInvoiceId": "650e8400-e29b-41d4-a716-446655440222", "convertedInvoiceNumber": "FAC-2026-050", "updatedAt": "2026-02-20T15:00:00Z" } } ``` ## State Changes ### Delivery Note Changes - **Status:** Changed from `draft` or `issued` → `converted` - **convertedAt:** Set to current UTC timestamp - **convertedInvoiceId:** Set to the UUID of the created invoice - **convertedInvoiceNumber:** Set to the invoice number for easy reference ### Invoice Creation - New invoice created with status `draft` - All line items copied from delivery note - Client, pricing, and terms copied from delivery note - `deliveryNoteId` and `deliveryNoteReference` fields set to link back to delivery note - Ready to be uploaded to ANAF ## Data Mapping The following fields are copied from delivery note to invoice: ### Basic Information - `clientId` - Same client - `currency` and `exchangeRate` - Same currency settings - `invoiceTypeCode` - Defaults to "380" (Commercial Invoice) ### Dates - `issueDate` - Defaults to today (can be overridden) - `dueDate` - Defaults to delivery note's due date (can be overridden) ### References - `deliveryLocation` - Delivery location from delivery note - `projectReference` - Project reference - `notes` - Public notes - `mentions` - Additional mentions - `issuerName` and `issuerId` - Issuer information - `salesAgent` - Sales agent ### Line Items - All line items with same details: - Description, quantity, unit price - VAT rate, unit of measure - Product reference - All calculations ### New Fields Added to Invoice - `deliveryNoteId` - UUID of source delivery note - `deliveryNoteReference` - Delivery note number for display ## Validation Rules ### Delivery Note Status Can convert delivery note in these statuses: - `draft` - Can convert immediately (uncommon) - `issued` - Recommended status for conversion Cannot convert delivery note in these statuses: - `cancelled` - Delivery was cancelled - `converted` - Already converted ### Data Validation - All delivery note data must be valid - Client must still exist - VAT rates must still exist - Products (if referenced) must still exist - Series must be valid for invoices ### Series Selection - If `invoiceSeriesId` is not provided, the company's default `invoice` series is auto-assigned to the created invoice - Series must be configured for outgoing invoices - Series must belong to the same company ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Delivery note not found or doesn't belong to the company | | 409 | `conflict` | Delivery note status prevents conversion or already converted | | 422 | `validation_error` | Invalid request body or delivery note data cannot be converted | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict - Already Converted ```json { "error": { "code": "conflict", "message": "Delivery note cannot be converted", "details": { "status": "converted", "reason": "Delivery note has already been converted to an invoice", "convertedAt": "2026-02-20T15:00:00Z", "convertedInvoiceId": "650e8400-e29b-41d4-a716-446655440222", "convertedInvoiceNumber": "FAC-2026-050" } } } ``` ### Status Conflict - Cancelled ```json { "error": { "code": "conflict", "message": "Delivery note cannot be converted", "details": { "status": "cancelled", "reason": "Cannot convert cancelled delivery note to invoice", "cancelledAt": "2026-02-19T09:15:00Z" } } } ``` ## Workflow Integration ### Standard Conversion Flow 1. Create delivery note (`POST /api/v1/delivery-notes`) 2. Issue delivery note (`POST /api/v1/delivery-notes/{uuid}/issue`) 3. Deliver goods/services 4. **Convert to invoice** (`POST /api/v1/delivery-notes/{uuid}/convert`) ← You are here 5. Upload invoice to ANAF (`POST /api/v1/invoices/{uuid}/upload`) 6. Send invoice to client ### Batch Conversion Flow For multiple deliveries to same client: 1. Issue multiple delivery notes throughout month 2. At month-end, convert all to separate invoices 3. OR manually create single invoice referencing all delivery notes ### Immediate Invoicing For immediate billing: 1. Create delivery note 2. **Convert immediately** (skip issue step) 3. Upload to ANAF 4. Issue goods with invoice ## Post-Conversion Actions After conversion, you should: 1. **Upload to ANAF** - Submit the invoice to ANAF e-Factura system 2. **Generate PDF** - Create PDF version for client 3. **Send to client** - Email invoice to client 4. **Update CRM** - Sync invoice status to CRM 5. **Track payment** - Monitor payment against this invoice 6. **Archive delivery note** - Keep delivery note as reference ## Conversion Timing ### When to Convert **Immediately after delivery:** - Standard practice for most deliveries - Client expects invoice soon after delivery - No batch invoicing arrangement **At period end:** - Monthly batch invoicing agreement - Multiple deliveries to accumulate - Client prefers consolidated invoices **Upon client request:** - Client-specific invoicing schedule - Project milestone completion - Accumulated value threshold reached **Before payment deadline:** - Ensure invoice issued in time for payment - Consider payment terms when timing conversion - Don't delay beyond client's processing needs ## Best Practices 1. **Convert promptly** - Don't delay invoicing after delivery 2. **Review before converting** - Ensure all delivery note data is correct 3. **Choose correct series** - Use appropriate invoice series 4. **Set correct dates** - Issue date typically = today or delivery date 5. **Link documents** - Delivery note reference is automatically maintained 6. **Upload quickly** - Submit to ANAF within required timeframe 7. **Notify stakeholders** - Alert accounting and sales teams 8. **Track conversions** - Monitor which delivery notes still need invoicing 9. **Batch strategically** - Group deliveries when it makes business sense 10. **Document flow** - Keep clear record of delivery note → invoice linkage ## Conversion Metrics Track these metrics for performance: - **Conversion rate** - % of delivery notes converted to invoices - **Time to conversion** - Days from issue to conversion - **Outstanding delivery notes** - Not yet converted - **Value not invoiced** - Total value in unconverted delivery notes - **Conversion by client** - Which clients have unconverted deliveries - **Aging analysis** - How old are unconverted delivery notes ## Reversing a Conversion If the invoice needs to be cancelled after conversion: 1. **Cannot "unconvert"** - The conversion is permanent 2. **Use credit note** - Create a credit note to reverse the invoice 3. **Delivery note remains converted** - Original delivery note status doesn't change 4. **Create new delivery note** - If needed for corrected delivery The bidirectional link between delivery note and invoice is maintained for audit trail purposes. ## Multiple Deliveries to One Invoice For batch invoicing scenarios: ### Option 1: Multiple Conversions - Convert each delivery note to separate invoice - Client receives multiple invoices - Each invoice linked to its delivery note ### Option 2: Manual Invoice Creation - Keep delivery notes as reference - Manually create invoice listing all deliveries - Reference all delivery note numbers in invoice notes - Don't use convert endpoint (delivery notes remain issued) ### Option 3: Periodic Summary Invoice - Accumulate delivered items throughout period - Create single invoice with summary line items - Reference delivery note numbers in mentions - Delivery notes remain issued (not converted) ## Integration Points ### Accounting Systems - Sync invoice creation to accounting - Link to delivery documentation - Update revenue recognition - Track receivables ### Warehouse Management - Confirm inventory deduction - Close delivery records - Update stock cards - Archive delivery documentation ### Payment Tracking - Create payment tracking record - Monitor invoice payment status - Send payment reminders - Update delivery note with payment info ## Compliance Notes ### Tax Implications - Invoice date determines tax period - Must match or follow delivery date - Required for VAT deduction by client - Keep delivery note as supporting documentation ### Retention Period - Keep both delivery note and invoice - Maintain clear linkage between documents - Minimum 5-10 years retention - Both physical and digital copies ## After Conversion Next steps: 1. Verify invoice created correctly 2. Review invoice details match delivery 3. Upload to ANAF immediately 4. Generate PDF for client 5. Send invoice to client with delivery note reference 6. Set up payment tracking 7. Update all integrated systems --- ## Create Delivery Note > Create a new delivery note to document delivery of goods or completion of services URL: https://docs.storno.ro/api-reference/delivery-notes/create # Create Delivery Note Creates a new delivery note in draft status. Delivery notes document the physical delivery of goods or completion of services, and can later be converted into invoices for payment. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `clientId` | string | Yes | UUID of the client | | `documentSeriesId` | string | No | UUID of the delivery note series. If not provided, the default `delivery_note` series for the company is auto-assigned | | `issueDate` | string | Yes | Date of issue (YYYY-MM-DD) | | `dueDate` | string | Yes | Due date for invoicing (YYYY-MM-DD) | | `currency` | string | Yes | Currency code (RON, EUR, USD, etc.) | | `exchangeRate` | number | No | Exchange rate (defaults to 1.0 for RON) | | `deliveryLocation` | string | No | Full address where goods delivered | | `projectReference` | string | No | Related project or order reference | | `issuerName` | string | No | Name of person issuing the delivery note | | `issuerId` | string | No | UUID of the issuer user | | `salesAgent` | string | No | Sales agent name | | `deputyName` | string | No | Name of person receiving the delivery | | `deputyIdentityCard` | string | No | ID card number of deputy | | `deputyAuto` | string | No | Vehicle registration number | | `notes` | string | No | Public notes about the delivery | | `mentions` | string | No | Additional mentions or instructions | | `internalNote` | string | No | Internal note (not visible to client) | | `lines` | array | Yes | Array of line items (minimum 1 item) | ### e-Transport Fields These fields are required for ANAF e-Transport submission. They can be set at creation time or updated later before submission. | Field | Type | Required | Description | |-------|------|----------|-------------| | `etransportOperationType` | number | No | Operation type: `10` (intra-EU acquisition), `12` (intra-EU delivery), `20` (domestic transaction), `30` (TTN — domestic transport document), `40` (import), `50` (export), `60` (goods not released for circulation) | | `etransportPostIncident` | boolean | No | Post-incident declaration (after the transport was completed) | | `etransportVehicleNumber` | string | No | Vehicle registration number (3-20 uppercase alphanumeric chars per BR-031, e.g., `"BC01ABC"`) | | `etransportTrailer1` | string | No | First trailer registration number | | `etransportTrailer2` | string | No | Second trailer registration number | | `etransportTransporterCountry` | string | No | Transporter country code (ISO 3166-1 alpha-2). Must be `"RO"` for TTN (opType 30) per BR-005 | | `etransportTransporterCode` | string | No | Transporter CUI/CIF (numeric only, e.g., `"31385365"`). Must match ANAF format per BR-002 | | `etransportTransporterName` | string | No | Transporter legal name | | `etransportTransportDate` | string | No | Transport start date (YYYY-MM-DD) | | `etransportStartCounty` | number | No | Start county code (1-52 per ANAF nomenclature, e.g., `4`=Bacau, `40`=Bucuresti). Required per BR-210 | | `etransportStartLocality` | string | No | Start locality name (2-100 chars per BR-214) | | `etransportStartStreet` | string | No | Start street name (2-100 chars per BR-215) | | `etransportStartNumber` | string | No | Start street number | | `etransportStartOtherInfo` | string | No | Start additional address info | | `etransportStartPostalCode` | string | No | Start postal code | | `etransportEndCounty` | number | No | End county code (1-52). Required per BR-211 | | `etransportEndLocality` | string | No | End locality name (2-100 chars) | | `etransportEndStreet` | string | No | End street name (2-100 chars) | | `etransportEndNumber` | string | No | End street number | | `etransportEndOtherInfo` | string | No | End additional address info | | `etransportEndPostalCode` | string | No | End postal code | ### Line Item Object | Field | Type | Required | Description | |-------|------|----------|-------------| | `description` | string | Yes | Item description | | `quantity` | number | Yes | Quantity delivered (must be > 0) | | `unitPrice` | number | Yes | Price per unit | | `vatRateId` | string | Yes | UUID of the VAT rate | | `unitOfMeasure` | string | No | Unit of measure (e.g., piece, kg, hour) | | `productId` | string | No | UUID of related product | | `tariffCode` | string | No | 8-digit HS/CN tariff code (e.g., `"84131100"`). Required for e-Transport per BR-206 | | `purposeCode` | number | No | Purpose code. For TTN (opType 30): `101` (commercial), `704`, `705`, `9901` (other) per BR-070 | | `unitOfMeasureCode` | string | No | UN/ECE Rec 20 unit code (e.g., `"H87"`=piece, `"KGM"`=kg, `"SET"`=set, `"MTR"`=meter) | | `netWeight` | string | No | Net weight in kg as decimal (e.g., `"120.00"`). Required for e-Transport per BR-207 | | `grossWeight` | string | No | Gross weight in kg as decimal (e.g., `"140.00"`) | | `valueWithoutVat` | string | No | Line value without VAT as decimal (e.g., `"15000.00"`). Required for e-Transport per BR-208 | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/delivery-notes \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "clientId": "750e8400-e29b-41d4-a716-446655440000", "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "issueDate": "2026-02-18", "dueDate": "2026-03-18", "currency": "RON", "exchangeRate": 1.0, "deliveryLocation": "Client warehouse - Str. Depozit 5, București, Sector 2", "projectReference": "PROJECT-2026-002", "issuerName": "John Doe", "salesAgent": "Jane Smith", "deputyName": "Maria Ionescu", "deputyIdentityCard": "AB123456", "deputyAuto": "B-123-ABC", "notes": "Handle with care - fragile items", "mentions": "Special delivery instructions: Use loading dock B", "internalNote": "Priority client - ensure careful handling", "lines": [ { "description": "Laptop Dell Latitude 7420", "quantity": 10, "unitPrice": 450, "unitOfMeasure": "piece", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "B50e8400-e29b-41d4-a716-446655440000" }, { "description": "Wireless Mouse Logitech MX Master 3", "quantity": 10, "unitPrice": 50, "unitOfMeasure": "piece", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "B60e8400-e29b-41d4-a716-446655440000" } ] }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: '750e8400-e29b-41d4-a716-446655440000', documentSeriesId: '850e8400-e29b-41d4-a716-446655440000', issueDate: '2026-02-18', dueDate: '2026-03-18', currency: 'RON', exchangeRate: 1.0, deliveryLocation: 'Client warehouse - Str. Depozit 5, București, Sector 2', projectReference: 'PROJECT-2026-002', issuerName: 'John Doe', salesAgent: 'Jane Smith', deputyName: 'Maria Ionescu', deputyIdentityCard: 'AB123456', deputyAuto: 'B-123-ABC', notes: 'Handle with care - fragile items', mentions: 'Special delivery instructions: Use loading dock B', internalNote: 'Priority client - ensure careful handling', lines: [ { description: 'Laptop Dell Latitude 7420', quantity: 10, unitPrice: 450, unitOfMeasure: 'piece', vatRateId: '350e8400-e29b-41d4-a716-446655440000', productId: 'B50e8400-e29b-41d4-a716-446655440000' }, { description: 'Wireless Mouse Logitech MX Master 3', quantity: 10, unitPrice: 50, unitOfMeasure: 'piece', vatRateId: '350e8400-e29b-41d4-a716-446655440000', productId: 'B60e8400-e29b-41d4-a716-446655440000' } ] }) }); const data = await response.json(); ``` ## Response ```json { "uuid": "950e8400-e29b-41d4-a716-446655440000", "number": "DN-2026-012", "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "DN", "nextNumber": 13, "prefix": "DN-", "year": 2026 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "address": "Str. Exemplu 123, București", "email": "contact@client.ro" }, "status": "draft", "issueDate": "2026-02-18", "dueDate": "2026-03-18", "currency": "RON", "exchangeRate": 1.0, "deliveryLocation": "Client warehouse - Str. Depozit 5, București, Sector 2", "projectReference": "PROJECT-2026-002", "issuerName": "John Doe", "salesAgent": "Jane Smith", "deputyName": "Maria Ionescu", "deputyIdentityCard": "AB123456", "deputyAuto": "B-123-ABC", "notes": "Handle with care - fragile items", "mentions": "Special delivery instructions: Use loading dock B", "internalNote": "Priority client - ensure careful handling", "lines": [ { "uuid": "A10e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Laptop Dell Latitude 7420", "quantity": "10.00", "unitPrice": "450.00", "unitOfMeasure": "piece", "productId": "B50e8400-e29b-41d4-a716-446655440000", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "subtotal": "4500.00", "vatAmount": "855.00", "total": "5355.00" }, { "uuid": "A20e8400-e29b-41d4-a716-446655440000", "lineNumber": 2, "description": "Wireless Mouse Logitech MX Master 3", "quantity": "10.00", "unitPrice": "50.00", "unitOfMeasure": "piece", "productId": "B60e8400-e29b-41d4-a716-446655440000", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "subtotal": "500.00", "vatAmount": "95.00", "total": "595.00" } ], "subtotal": "5000.00", "vatAmount": "950.00", "total": "5950.00", "issuedAt": null, "convertedAt": null, "convertedInvoiceId": null, "cancelledAt": null, "createdAt": "2026-02-18T09:00:00Z", "updatedAt": "2026-02-18T09:00:00Z" } ``` ## Validation Rules ### Dates - `issueDate` must be a valid date in YYYY-MM-DD format - `issueDate` should not be in the future - `dueDate` must be equal to or after `issueDate` ### Currency - Must be a valid 3-letter currency code (ISO 4217) - `exchangeRate` must be greater than 0 - For RON (base currency), `exchangeRate` defaults to 1.0 ### Line Items - Minimum 1 line item required - `quantity` must be greater than 0 - `unitPrice` must be greater than or equal to 0 - `vatRateId` must reference an existing VAT rate - If `productId` is provided, it must reference an existing product ### References - `clientId` must reference an existing client - If `documentSeriesId` is provided, it must reference an existing series configured for `delivery_note` type; if omitted, the company's default `delivery_note` series is auto-assigned - If `issuerId` is provided, it must reference an existing user ### Deputy Information - Optional but recommended for proof of delivery - `deputyIdentityCard` should be valid ID format - `deputyAuto` should be valid vehicle registration format ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid request body structure | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Referenced entity not found (client, series, VAT rate, product, user) | | 422 | `validation_error` | Validation failed (see error details for specific field errors) | | 500 | `internal_error` | Server error occurred | ## Example Validation Error Response ```json { "error": { "code": "validation_error", "message": "Validation failed", "details": { "clientId": ["Client not found"], "lines.0.quantity": ["Quantity must be greater than 0"], "lines.1.vatRateId": ["VAT rate not found"], "dueDate": ["Due date must be after issue date"] } } } ``` ## Common Scenarios ### Physical Goods Delivery ```javascript { deliveryLocation: "Client warehouse - Str. Depozit 5", deputyName: "Maria Ionescu", deputyIdentityCard: "AB123456", deputyAuto: "B-123-ABC", notes: "Delivery completed, all items verified", lines: [ { description: "Product A", quantity: 50, unitPrice: 10, ... } ] } ``` ### Service Completion ```javascript { deliveryLocation: "Client office - Str. Birouri 10", deputyName: "John Manager", notes: "IT support services completed successfully", lines: [ { description: "IT Support - 8 hours", quantity: 8, unitPrice: 75, ... } ] } ``` ### Batch Delivery (Multiple Items) ```javascript { deliveryLocation: "Construction site - Bd. Constructorilor 50", deputyName: "Site Manager", deputyAuto: "B-TRUCK-123", lines: [ { description: "Cement bags", quantity: 100, unitPrice: 25, ... }, { description: "Steel bars", quantity: 50, unitPrice: 120, ... }, { description: "Paint buckets", quantity: 20, unitPrice: 45, ... } ] } ``` ### e-Transport Ready (TTN Domestic) ```javascript { clientId: "750e8400...", issueDate: "2026-03-04", dueDate: "2026-04-04", currency: "RON", deliveryLocation: "Depozit Bacau, Str. Industriei 15", deputyName: "Vasile Popescu", deputyIdentityCard: "BC123456", deputyAuto: "BC01ABC", // e-Transport header etransportOperationType: 30, etransportVehicleNumber: "BC01ABC", etransportTransporterCountry: "RO", etransportTransporterCode: "31385365", etransportTransporterName: "UNIVERSAL EQUIPMENT PROJECTS SRL", etransportTransportDate: "2026-03-04", // Route start etransportStartCounty: 4, etransportStartLocality: "Bacau", etransportStartStreet: "Str. Nicolae Balcescu 5 B", etransportStartNumber: "5B", etransportStartPostalCode: "600001", // Route end etransportEndCounty: 40, etransportEndLocality: "SECTOR6", etransportEndStreet: "Sos. Virtutii 148", etransportEndNumber: "148", etransportEndPostalCode: "060784", lines: [ { description: "Echipament hidraulic EH-200", quantity: 2, unitPrice: 15000, vatRateId: "...", tariffCode: "84131100", purposeCode: 101, unitOfMeasureCode: "H87", netWeight: "240.00", grossWeight: "280.00", valueWithoutVat: "30000.00" } ] } ``` ### International Delivery ```javascript { deliveryLocation: "Budapest warehouse - Warehouse District 15", currency: "EUR", exchangeRate: 4.97, deputyName: "Import Manager", deputyIdentityCard: "HU-12345678", deputyAuto: "HU-AB-1234", mentions: "Customs clearance completed", lines: [ { description: "Equipment Model X", quantity: 5, unitPrice: 2000, ... } ] } ``` ## Best Practices 1. **Create at delivery time** - Issue delivery note when delivery occurs, not before 2. **Complete deputy info** - Always capture who received the delivery 3. **Specific locations** - Use exact delivery address, not just client's billing address 4. **Clear descriptions** - Detailed line item descriptions for verification 5. **Link to orders** - Use `projectReference` to link to purchase orders 6. **Track vehicles** - Record `deputyAuto` for logistics tracking 7. **Prompt issuing** - Mark as issued immediately after delivery 8. **Convert timely** - Don't delay invoicing after successful delivery 9. **Photo evidence** - Keep photos of signed physical delivery notes 10. **Internal notes** - Use `internalNote` for logistics or special handling notes ## Delivery Note vs Proforma vs Invoice ### Delivery Note - **Purpose:** Document physical delivery or service completion - **Timing:** Created at delivery time - **Legal status:** Proof of delivery, not a payment request - **Next step:** Convert to invoice for payment ### Proforma Invoice - **Purpose:** Quote or offer before delivery - **Timing:** Created before delivery - **Legal status:** Not legally binding - **Next step:** Convert to invoice after acceptance ### Invoice - **Purpose:** Request payment for delivered goods/services - **Timing:** Created after delivery (or from delivery note) - **Legal status:** Legally binding payment request - **Next step:** Upload to ANAF, send to client ## Next Steps After creating a delivery note: 1. Mark as issued when delivery occurs (`POST /api/v1/delivery-notes/{uuid}/issue`) 2. Get client signature on physical copy 3. Convert to invoice when ready to bill (`POST /api/v1/delivery-notes/{uuid}/convert`) 4. Upload invoice to ANAF 5. Send invoice to client for payment --- ## Create Delivery Note from Proforma > Create a new delivery note from an existing proforma invoice URL: https://docs.storno.ro/api-reference/delivery-notes/from-proforma # Create Delivery Note from Proforma Creates a new delivery note by copying data from an existing proforma invoice. The client, line items, dates, currency, and notes are all copied from the proforma. The new delivery note is created in `draft` status. The company's default `delivery_note` series is automatically assigned to the new delivery note. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `proformaId` | string | Yes | UUID of the proforma invoice to copy from | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/delivery-notes/from-proforma \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "proformaId": "720e8400-e29b-41d4-a716-446655440000" }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/from-proforma', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ proformaId: '720e8400-e29b-41d4-a716-446655440000' }) }); const deliveryNote = await response.json(); ``` ## Response Returns the newly created delivery note in `draft` status: ```json { "uuid": "950e8400-e29b-41d4-a716-446655440000", "number": "DN-2026-013", "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "DN", "nextNumber": 14 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "email": "contact@client.ro" }, "status": "draft", "issueDate": "2026-02-19", "dueDate": "2026-03-19", "currency": "RON", "exchangeRate": 1.0, "notes": "Based on proforma PRO-2026-008", "lines": [ { "uuid": "A40e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Laptop Dell Latitude 7420", "quantity": "10.00", "unitPrice": "450.00", "unitOfMeasure": "piece", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "percentage": "19.00" }, "subtotal": "4500.00", "vatAmount": "855.00", "total": "5355.00" } ], "subtotal": "4500.00", "vatAmount": "855.00", "total": "5355.00", "createdAt": "2026-02-19T10:00:00Z", "updatedAt": "2026-02-19T10:00:00Z" } ``` ## Data Copied from Proforma | Field | Copied | |-------|--------| | `clientId` | Yes | | `currency` / `exchangeRate` | Yes | | `issueDate` | Yes (set to today) | | `dueDate` | Yes | | `notes` | Yes | | `lines` (all items) | Yes | | `deliveryLocation` | Yes | | `projectReference` | Yes | | `documentSeriesId` | No — the company's default `delivery_note` series is auto-assigned | ## Validation Rules - `proformaId` must be a valid UUID of a proforma belonging to the same company - The proforma must have at least one line item - The referenced client must still exist ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Proforma invoice not found or doesn't belong to the company | | 422 | `validation_error` | Invalid `proformaId` or proforma has no lines | | 500 | `internal_error` | Server error occurred | ## Workflow Integration ### Proforma to Delivery Note Flow 1. Create proforma invoice (`POST /api/v1/proforma-invoices`) 2. Send proforma to client for approval 3. **Create delivery note from proforma** (`POST /api/v1/delivery-notes/from-proforma`) ← You are here 4. Adjust the delivery note if quantities changed 5. Issue the delivery note (`POST /api/v1/delivery-notes/{uuid}/issue`) 6. Convert to invoice after delivery (`POST /api/v1/delivery-notes/{uuid}/convert`) ## Related Endpoints - [Create delivery note](/api-reference/delivery-notes/create) - Create a delivery note manually - [Issue delivery note](/api-reference/delivery-notes/issue) - Issue the new draft - [Update delivery note](/api-reference/delivery-notes/update) - Adjust before issuing --- ## Create Storno Delivery Note > Create a storno (return) delivery note with negated quantities URL: https://docs.storno.ro/api-reference/delivery-notes/storno # Create Storno Delivery Note Creates a storno (return) delivery note from an existing issued delivery note. All line item quantities are negated to represent the return of goods. The new delivery note is created in `draft` status and references the original in its `notes` field. The company's default `delivery_note` series is automatically assigned to the new storno delivery note. Only delivery notes in `issued` status can be stornoed. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the issued delivery note to storno | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/storno \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/storno', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const stornoNote = await response.json(); ``` ## Response Returns the newly created storno delivery note in `draft` status: ```json { "uuid": "A60e8400-e29b-41d4-a716-446655440001", "number": "DN-2026-013", "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "DN", "nextNumber": 14 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "email": "contact@client.ro" }, "status": "draft", "issueDate": "2026-02-19", "dueDate": "2026-03-19", "currency": "RON", "exchangeRate": 1.0, "notes": "Storno aviz DN-2026-012", "lines": [ { "uuid": "B10e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Laptop Dell Latitude 7420", "quantity": "-10.00", "unitPrice": "450.00", "unitOfMeasure": "piece", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "percentage": "19.00" }, "subtotal": "-4500.00", "vatAmount": "-855.00", "total": "-5355.00" } ], "subtotal": "-4500.00", "vatAmount": "-855.00", "total": "-5355.00", "createdAt": "2026-02-19T10:00:00Z", "updatedAt": "2026-02-19T10:00:00Z" } ``` ## State Changes ### New Storno Delivery Note - Created in `draft` status with a new UUID and number - All line item quantities are negated (positive → negative) - All totals are negative - `notes` field references the original delivery note number - Issue date is set to today; due date copied from original - The company's default `delivery_note` series is automatically assigned as `documentSeriesId` ### Original Delivery Note - Remains unchanged in `issued` status - No fields are modified on the original ## Validation Rules - The source delivery note must be in `issued` status - Cannot storno a `draft`, `cancelled`, or `converted` delivery note ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Delivery note not found or doesn't belong to the company | | 422 | `validation_error` | Delivery note is not in issued status | | 500 | `internal_error` | Server error occurred | ## Example Error Response ### Not Issued ```json { "error": { "code": "validation_error", "message": "Delivery note cannot be stornoed", "details": { "status": "draft", "reason": "Only issued delivery notes can be stornoed" } } } ``` ## Workflow Integration ### Storno Flow 1. Issue a delivery note (`POST /api/v1/delivery-notes/{uuid}/issue`) 2. Goods are returned by the client 3. **Create storno** (`POST /api/v1/delivery-notes/{uuid}/storno`) ← You are here 4. Review and edit the storno delivery note if needed 5. Issue the storno delivery note (`POST /api/v1/delivery-notes/{uuid}/issue`) ## Related Endpoints - [Issue delivery note](/api-reference/delivery-notes/issue) - Issue the new storno draft - [Update delivery note](/api-reference/delivery-notes/update) - Edit before issuing - [Get delivery note](/api-reference/delivery-notes/get) - View the original delivery note --- ## Delete Delivery Note > Permanently delete a delivery note (draft status only) URL: https://docs.storno.ro/api-reference/delivery-notes/delete # Delete Delivery Note Permanently deletes a delivery note and all its line items. Only delivery notes in `draft` status can be deleted. Once issued, converted, or cancelled, a delivery note cannot be deleted. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the delivery note to delete | ## Request ```bash {% title="cURL" %} curl -X DELETE https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000', { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); // Success: 204 No Content (no response body) if (response.status === 204) { console.log('Delivery note deleted successfully'); } ``` ## Response **Success:** Returns `204 No Content` with an empty response body. The delivery note and all associated line items are permanently deleted from the database. ## Restrictions ### Status Requirement Only delivery notes with `status = draft` can be deleted. Delivery notes in the following states **cannot** be deleted: - `issued` - Already issued and delivered - `converted` - Converted to invoice - `cancelled` - Already cancelled For non-draft delivery notes, use the [cancel endpoint](/api-reference/delivery-notes/cancel) instead. ### Referential Integrity - Deleting a delivery note does not affect the series number counter - The series counter is not rolled back - If the delivery note was converted to an invoice, the invoice is **not** deleted ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Delivery note not found or doesn't belong to the company | | 409 | `conflict` | Delivery note status prevents deletion (not in draft) | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict - Issued ```json { "error": { "code": "conflict", "message": "Delivery note cannot be deleted", "details": { "status": "issued", "reason": "Only draft delivery notes can be deleted. Use cancel instead.", "issuedAt": "2026-02-18T14:30:00Z" } } } ``` ### Status Conflict - Converted ```json { "error": { "code": "conflict", "message": "Delivery note cannot be deleted", "details": { "status": "converted", "reason": "Delivery note has been converted to an invoice", "convertedAt": "2026-02-20T10:00:00Z", "convertedInvoiceId": "650e8400-e29b-41d4-a716-446655440222", "convertedInvoiceNumber": "FAC-2026-050" } } } ``` ### Not Found ```json { "error": { "code": "not_found", "message": "Delivery note not found" } } ``` ## Best Practices ### When to Delete vs Cancel **Delete** when: - The delivery note was created by mistake - The delivery note is still in draft and hasn't been processed - You want to remove all traces of the document - No physical delivery has occurred - Easier to recreate than to update **Cancel** when: - The delivery note has been issued - The delivery note has been shared with client or logistics - You need to maintain audit trail - Physical delivery was planned but cancelled - Historical records needed for reporting ### Audit Considerations Deleted delivery notes: - Are permanently removed from the database - Do not appear in reports or exports - Cannot be recovered - Do not leave audit trail entries - Do not affect series counter For compliance and audit purposes, consider using the cancel endpoint instead of delete, especially for delivery notes that were shared or processed. ## Common Deletion Scenarios ### Scenario 1: Duplicate Creation ``` Problem: Accidentally created same delivery note twice Solution: Delete the duplicate draft delivery note ``` ### Scenario 2: Wrong Client ``` Problem: Created delivery note for wrong client Solution: Delete draft, create new one with correct client ``` ### Scenario 3: Data Entry Errors ``` Problem: Multiple errors, easier to recreate Solution: Delete draft, create fresh delivery note ``` ### Scenario 4: Order Cancelled Before Preparation ``` Problem: Client cancelled before delivery was prepared Solution: Delete draft delivery note (no physical process started) ``` ## Impact Analysis ### What Gets Deleted - Delivery note record - All line items - Associated metadata ### What Remains Unchanged - Client record - Product records - Series configuration - Series counter (next number not rolled back) - Related proformas or invoices (if any) ## Recovery from Accidental Deletion If you accidentally delete a draft delivery note: 1. **Cannot recover** - Deletion is permanent 2. **Recreate from scratch** - Use client and product data 3. **Check backups** - If database backups exist 4. **Review logs** - Check application logs for deleted data 5. **Prevent future errors** - Implement confirmation dialogs ## Alternative Actions Instead of deleting, consider: ### Update the Delivery Note If data is mostly correct but needs changes: ``` PUT /api/v1/delivery-notes/{uuid} ``` ### Cancel the Delivery Note If delivery won't occur but needs audit trail: ``` POST /api/v1/delivery-notes/{uuid}/cancel ``` ### Keep as Draft If uncertain about delivery timing: - Leave in draft status - Add internal notes about status - Update when delivery is confirmed ## Deletion vs Other Operations | Operation | Draft | Issued | Converted | Effect | Audit Trail | |-----------|-------|--------|-----------|--------|-------------| | **Delete** | ✓ | ✗ | ✗ | Permanent removal | None | | **Cancel** | ✓ | ✓ | ✗ | Status change | Full | | **Update** | ✓ | ✗ | ✗ | Modify data | Change log | ## Best Practices Summary 1. **Delete only drafts** - Never attempt to delete issued delivery notes 2. **Verify before deleting** - Double-check you're deleting the right document 3. **Log the action** - Record why delivery note was deleted in external system 4. **Recreate if needed** - Create new correct delivery note immediately 5. **Use cancel for issued** - If delivery note was issued, cancel instead 6. **Check references** - Ensure no other documents reference this delivery note 7. **Review process** - Understand why incorrect delivery note was created ## After Deletion Once deleted: 1. **Verify removal** - Check that delivery note is removed from list 2. **Create replacement** - If delivery is still needed 3. **Update documentation** - Note the deletion in related records 4. **Notify stakeholders** - If delivery note was expected by others 5. **Review inventory** - Ensure inventory status is correct 6. **Check logistics** - Cancel any related shipment preparations --- ## Download Delivery Note PDF > Download the PDF representation of a delivery note URL: https://docs.storno.ro/api-reference/delivery-notes/pdf # Download Delivery Note PDF Downloads the PDF for a delivery note. The PDF is generated using the company's configured PDF template and includes all line items, client details, and delivery information. ``` GET /api/v1/delivery-notes/{uuid}/pdf ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token or API key for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | UUID of the delivery note | ## Request ```bash {% title="cURL" %} curl https://api.storno.ro/api/v1/delivery-notes/7c9e6679-7425-40de-944b-e07fc1f90ae7/pdf \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -o aviz.pdf ``` ```javascript {% title="JavaScript" %} const response = await fetch( 'https://api.storno.ro/api/v1/delivery-notes/7c9e6679-7425-40de-944b-e07fc1f90ae7/pdf', { headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } } ); const blob = await response.blob(); ``` ## Response Returns the PDF file as binary data with appropriate headers. **Headers:** | Header | Value | |--------|-------| | `Content-Type` | `application/pdf` | | `Content-Disposition` | `attachment; filename="aviz-{number}.pdf"` | ## Error Codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Delivery note not found | | `500` | PDF generation failed | --- ## Get Delivery Note > Retrieve detailed information for a specific delivery note including line items URL: https://docs.storno.ro/api-reference/delivery-notes/get # Get Delivery Note Retrieves complete details for a specific delivery note, including all line items, client information, delivery details, and calculated totals. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the delivery note to retrieve | ## Request ```bash {% title="cURL" %} curl -X GET https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response ```json { "uuid": "950e8400-e29b-41d4-a716-446655440000", "number": "DN-2026-012", "seriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "DN", "nextNumber": 13, "prefix": "DN-", "year": 2026 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "address": "Str. Exemplu 123, București, Sector 1", "city": "București", "county": "București", "country": "RO", "postalCode": "010101", "email": "contact@client.ro", "phone": "+40721234567", "bankAccount": "RO49AAAA1B31007593840000", "bankName": "Banca Comercială Română" }, "status": "issued", "issueDate": "2026-02-18", "dueDate": "2026-03-18", "currency": "RON", "exchangeRate": 1.0, "deliveryLocation": "Client warehouse - Str. Depozit 5, București, Sector 2", "projectReference": "PROJECT-2026-002", "issuerName": "John Doe", "issuerId": "850e8400-e29b-41d4-a716-446655440000", "salesAgent": "Jane Smith", "deputyName": "Maria Ionescu", "deputyIdentityCard": "AB123456", "deputyAuto": "B-123-ABC", "notes": "Handle with care - fragile items. Delivery completed successfully.", "mentions": "Special delivery instructions: Use loading dock B", "internalNote": "Priority client - ensure careful handling", "lines": [ { "uuid": "A10e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Laptop Dell Latitude 7420", "quantity": "10.00", "unitPrice": "450.00", "unitOfMeasure": "piece", "productId": "B50e8400-e29b-41d4-a716-446655440000", "product": { "uuid": "B50e8400-e29b-41d4-a716-446655440000", "name": "Laptop Dell Latitude 7420", "code": "LAP-DELL-7420", "unitOfMeasure": "piece" }, "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "subtotal": "4500.00", "vatAmount": "855.00", "total": "5355.00" }, { "uuid": "A20e8400-e29b-41d4-a716-446655440000", "lineNumber": 2, "description": "Wireless Mouse Logitech MX Master 3", "quantity": "10.00", "unitPrice": "50.00", "unitOfMeasure": "piece", "productId": "B60e8400-e29b-41d4-a716-446655440000", "product": { "uuid": "B60e8400-e29b-41d4-a716-446655440000", "name": "Wireless Mouse Logitech MX Master 3", "code": "MOUSE-LOG-MX3", "unitOfMeasure": "piece" }, "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "subtotal": "500.00", "vatAmount": "95.00", "total": "595.00" } ], "subtotal": "5000.00", "vatAmount": "950.00", "total": "5950.00", "issuedAt": "2026-02-18T14:30:00Z", "convertedAt": null, "convertedInvoiceId": null, "convertedInvoiceNumber": null, "cancelledAt": null, "createdAt": "2026-02-18T09:00:00Z", "updatedAt": "2026-02-18T14:30:00Z" } ``` ## Response Fields ### Core Information | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `number` | string | Delivery note number | | `status` | string | Current status (draft/issued/converted/cancelled) | | `series` | object | Series information with prefix and year | | `client` | object | Complete client details including banking information | ### Delivery Information | Field | Type | Description | |-------|------|-------------| | `deliveryLocation` | string | Full address where goods were delivered | | `deputyName` | string | Name of person who received the delivery | | `deputyIdentityCard` | string | ID card number of the deputy | | `deputyAuto` | string | Vehicle registration number used for delivery | | `issuerName` | string | Name of person who issued the delivery note | | `issuerId` | string | UUID of the issuer user | | `salesAgent` | string | Sales agent responsible for the delivery | ### Financial Information | Field | Type | Description | |-------|------|-------------| | `currency` | string | Currency code (e.g., RON, EUR, USD) | | `exchangeRate` | number | Exchange rate to base currency | | `subtotal` | string | Subtotal before VAT | | `vatAmount` | string | Total VAT amount | | `total` | string | Grand total including VAT | | `lines` | array | Array of line items with products and pricing | ### Dates and Status | Field | Type | Description | |-------|------|-------------| | `issueDate` | string | Date when delivery note was created | | `dueDate` | string | Due date for converting to invoice | | `issuedAt` | string \| null | Timestamp when issued | | `convertedAt` | string \| null | Timestamp when converted to invoice | | `cancelledAt` | string \| null | Timestamp when cancelled | | `convertedInvoiceId` | string \| null | UUID of created invoice (if converted) | | `convertedInvoiceNumber` | string \| null | Number of created invoice (if converted) | ### Additional Information | Field | Type | Description | |-------|------|-------------| | `projectReference` | string | Related project or order reference | | `notes` | string | Public notes about the delivery | | `mentions` | string | Additional mentions or instructions | | `internalNote` | string | Internal note (not visible to client) | ### Line Item Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Line item unique identifier | | `lineNumber` | integer | Sequential line number | | `description` | string | Item description | | `quantity` | string | Quantity delivered (decimal string) | | `unitPrice` | string | Price per unit | | `unitOfMeasure` | string | Unit of measure (e.g., piece, kg, hour) | | `product` | object \| null | Related product details | | `vatRate` | object | VAT rate details with percentage | | `subtotal` | string | Line subtotal (before VAT) | | `vatAmount` | string | Line VAT amount | | `total` | string | Line total (including VAT) | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Delivery note not found or doesn't belong to the company | | 500 | `internal_error` | Server error occurred | ## Deputy Information ### Why Deputy Details Matter Deputy information serves as proof of delivery: - **deputyName** - Who physically received the goods/services - **deputyIdentityCard** - Legal identification for verification - **deputyAuto** - Vehicle used (for transport companies or logistics) This information is especially important for: - Legal proof of delivery - Customs documentation - Transport documentation (CMR) - Dispute resolution - Insurance claims ### When Deputy Info is Required - Physical goods delivery - High-value items - Cross-border shipments - Regulated industries - Client request ### When Deputy Info is Optional - Digital services - Remote services - Small-value deliveries - Trusted client relationships ## Delivery Location vs Client Address The `deliveryLocation` field can differ from the client's registered address: - **Client address** - Legal/billing address - **Delivery location** - Physical delivery point (warehouse, job site, etc.) Always use the specific delivery location for: - Accurate logistics - Client verification - Future deliveries reference - Customs documentation ## Conversion to Invoice When the delivery note is converted to an invoice: - `status` changes to `converted` - `convertedAt` is set to the conversion timestamp - `convertedInvoiceId` contains the new invoice UUID - `convertedInvoiceNumber` contains the new invoice number - Delivery note data is copied to the invoice - Delivery note reference is added to invoice ## Use Cases ### Standard Goods Delivery ``` 1. Issue delivery note upon shipment 2. Client receives and deputy signs 3. Convert to invoice after verification ``` ### Service Completion ``` 1. Create delivery note when service complete 2. Record completion location and verifier 3. Convert to invoice for payment ``` ### Batch Deliveries ``` 1. Multiple delivery notes throughout month 2. Client accumulates deliveries 3. Convert all at month-end to single invoice ``` ### International Shipments ``` 1. Delivery note with full deputy details 2. Used for customs clearance 3. Convert after successful delivery ``` ## Best Practices 1. **Complete deputy information** - Always capture for proof of delivery 2. **Accurate locations** - Use specific delivery addresses, not just client address 3. **Prompt updates** - Mark as issued when delivery occurs 4. **Photo evidence** - Keep photos of signed delivery notes 5. **Vehicle tracking** - Record vehicle registration for logistics 6. **Convert regularly** - Don't delay invoicing after delivery 7. **Link to orders** - Use projectReference to link to purchase orders 8. **Clear descriptions** - Detailed line item descriptions for verification --- ## Get Email Defaults > Get pre-filled email content for a delivery note URL: https://docs.storno.ro/api-reference/delivery-notes/email-defaults # Get Email Defaults Returns pre-filled email content for a delivery note based on the company's configured email template. All template variables are already substituted with real values from the delivery note and client records. Use this to populate the email form before sending. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the delivery note | ## Request ```bash {% title="cURL" %} curl https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/email-defaults \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/email-defaults', { headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const defaults = await response.json(); // Use to pre-populate email form document.getElementById('email-to').value = defaults.to; document.getElementById('email-subject').value = defaults.subject; document.getElementById('email-body').value = defaults.body; ``` ## Response Returns an object with pre-filled `to`, `subject`, and `body` fields: ```json { "to": "contact@client.ro", "subject": "Aviz de livrare DN-2026-012 de la Your Company SRL", "body": "Buna ziua,\n\nGasiti atasat avizul de livrare DN-2026-012 in valoare de 5,950.00 RON.\n\nMultumim!\n\nYour Company SRL" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `to` | string | Client email address (from client record) | | `subject` | string | Pre-filled subject with all template variables replaced | | `body` | string | Pre-filled body with all template variables replaced | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Delivery note not found or doesn't belong to the company | ## Related Endpoints - [Send email](/api-reference/delivery-notes/email) - Send the email using these defaults - [Get email history](/api-reference/delivery-notes/email-history) - View previously sent emails --- ## Get Email History > Get email sending history for a delivery note URL: https://docs.storno.ro/api-reference/delivery-notes/email-history # Get Email History Returns the complete history of emails sent for a specific delivery note, ordered by sent date (newest first). ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the delivery note | ## Request ```bash {% title="cURL" %} curl https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/emails \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/emails', { headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const emailHistory = await response.json(); emailHistory.forEach(entry => { console.log(`Sent to ${entry.to} on ${entry.sentAt}: ${entry.deliveryStatus}`); }); ``` ## Response Returns an array of email log entries: ```json [ { "id": "8f9a0b1c-2d3e-4f5a-6b7c-8d9e0f1a2b3c", "to": "contact@client.ro", "cc": ["accounting@client.ro"], "bcc": null, "subject": "Aviz de livrare DN-2026-012", "attachments": ["aviz-DN-2026-012.pdf"], "sentAt": "2026-02-18T14:35:00Z", "deliveryStatus": "delivered", "openedAt": "2026-02-18T15:10:00Z", "bouncedAt": null, "bounceReason": null, "sentBy": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "John Doe", "email": "john@yourcompany.ro" } }, { "id": "7e8f9a0b-1c2d-3e4f-5a6b-7c8d9e0f1a2b", "to": "old@client.ro", "cc": null, "bcc": null, "subject": "Aviz de livrare DN-2026-012", "attachments": ["aviz-DN-2026-012.pdf"], "sentAt": "2026-02-18T09:00:00Z", "deliveryStatus": "bounced", "openedAt": null, "bouncedAt": "2026-02-18T09:01:00Z", "bounceReason": "Mailbox does not exist", "sentBy": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "John Doe", "email": "john@yourcompany.ro" } } ] ``` ### Email Record Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Email log UUID | | `to` | string | Primary recipient email address | | `cc` | string[]\|null | CC recipients | | `bcc` | string[]\|null | BCC recipients | | `subject` | string | Email subject line | | `attachments` | string[] | List of attached file names | | `sentAt` | string | ISO 8601 timestamp when email was sent | | `deliveryStatus` | string | Current delivery status (see values below) | | `openedAt` | string\|null | When the email was first opened | | `bouncedAt` | string\|null | When the email bounced | | `bounceReason` | string\|null | Reason for bounce | | `sentBy` | object | User who triggered the send | ### Delivery Status Values | Status | Description | |--------|-------------| | `queued` | Email is queued for sending | | `sent` | Sent to recipient's mail server | | `delivered` | Delivered to recipient's inbox | | `opened` | Email was opened by recipient | | `bounced` | Email bounced (hard or soft) | | `failed` | Email sending failed | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Delivery note not found or doesn't belong to the company | ## Related Endpoints - [Send email](/api-reference/delivery-notes/email) - Send a new email - [Get email defaults](/api-reference/delivery-notes/email-defaults) - Get pre-filled email content --- ## Issue Delivery Note > Mark a delivery note as issued when delivery occurs URL: https://docs.storno.ro/api-reference/delivery-notes/issue # Issue Delivery Note Marks a delivery note as issued when the physical delivery of goods or completion of services occurs. This action transitions the delivery note from `draft` status to `issued` status and records the timestamp. When the delivery note is issued, the next sequential number from its assigned `delivery_note` series is permanently assigned. If no series was explicitly set on the delivery note, the company's default `delivery_note` series is auto-found and used at this point. Once issued, the delivery note becomes read-only and can only be cancelled or converted to an invoice. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the delivery note to mark as issued | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/issue \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/issue', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response Returns the updated delivery note with `status = issued` and `issuedAt` timestamp: ```json { "uuid": "950e8400-e29b-41d4-a716-446655440000", "number": "DN-2026-012", "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "DN", "nextNumber": 13 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "email": "contact@client.ro" }, "status": "issued", "issueDate": "2026-02-18", "dueDate": "2026-03-18", "currency": "RON", "exchangeRate": 1.0, "subtotal": "5000.00", "vatAmount": "950.00", "total": "5950.00", "deliveryLocation": "Client warehouse - Str. Depozit 5, București", "projectReference": "PROJECT-2026-002", "deputyName": "Maria Ionescu", "deputyIdentityCard": "AB123456", "deputyAuto": "B-123-ABC", "notes": "Handle with care - fragile items", "issuedAt": "2026-02-18T14:30:00Z", "convertedAt": null, "convertedInvoiceId": null, "createdAt": "2026-02-18T09:00:00Z", "updatedAt": "2026-02-18T14:30:00Z" } ``` ## State Changes ### Status Transition - **Before:** `status = draft` - **After:** `status = issued` ### Series & Number Assignment - The next sequential number from the assigned `delivery_note` series is permanently locked in - If no `documentSeriesId` was set on the delivery note, the company's default `delivery_note` series is auto-found and assigned at this point - Once issued, the series and document number cannot be changed ### Timestamp - Sets `issuedAt` to current UTC timestamp (ISO 8601 format) - Updates `updatedAt` timestamp ### Restrictions Applied Once marked as issued: - Cannot be updated (PUT requests will fail) - Cannot be deleted (DELETE requests will fail) - Can be cancelled or converted to invoice ## Validation Rules ### Status Requirement - Delivery note must have `status = draft` - Cannot issue a delivery note that is already issued, converted, or cancelled ### Data Completeness Before issuing, ensure the delivery note has: - Valid client information - At least one line item - All required fields populated - Correct totals calculated - Delivery location specified (recommended) - Deputy information captured (recommended) ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Delivery note not found or doesn't belong to the company | | 409 | `conflict` | Delivery note status prevents issuing (not in draft status) | | 422 | `validation_error` | Delivery note data is incomplete or invalid | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict ```json { "error": { "code": "conflict", "message": "Delivery note cannot be issued", "details": { "status": "issued", "reason": "Delivery note is already marked as issued", "issuedAt": "2026-02-18T14:30:00Z" } } } ``` ### Validation Error ```json { "error": { "code": "validation_error", "message": "Delivery note data is incomplete", "details": { "deliveryLocation": ["Delivery location is recommended before issuing"], "deputyName": ["Deputy name is recommended for proof of delivery"], "lines": ["At least one line item is required"] } } } ``` ## When to Issue Issue the delivery note: - **At delivery time** - When goods are physically delivered - **Upon service completion** - When services are completed - **After client verification** - When client confirms receipt - **Before transport** - For shipments requiring delivery note ## Workflow Integration ### Standard Delivery Flow 1. Create delivery note (`POST /api/v1/delivery-notes`) 2. Load goods for delivery 3. Transport to delivery location 4. **Mark as issued** (`POST /api/v1/delivery-notes/{uuid}/issue`) ← You are here 5. Get client signature on physical copy 6. Convert to invoice (`POST /api/v1/delivery-notes/{uuid}/convert`) 7. Upload invoice to ANAF ### Service Completion Flow 1. Create delivery note for services 2. Perform services at client location 3. **Mark as issued** when work is complete 4. Client verifies completion 5. Convert to invoice for payment ### Batch Delivery Flow 1. Create multiple delivery notes 2. **Issue each** as deliveries occur 3. Accumulate throughout the month 4. Convert all to single invoice at month-end ## Physical Documentation After marking as issued: 1. **Print delivery note** - Generate PDF for signature 2. **Get client signature** - Physical proof of delivery 3. **Scan signed copy** - Archive digital copy 4. **Keep original** - Store signed original per regulations 5. **Send copy to client** - Email PDF copy for their records ## Proof of Delivery The issued delivery note serves as: - **Legal proof** - Evidence that goods were delivered - **Tax documentation** - Support for invoice - **Dispute resolution** - Reference in case of conflicts - **Logistics record** - Tracking of deliveries - **Customs documentation** - For international shipments ### Essential Information for Proof - Deputy name and signature - Deputy ID card number - Date and time of delivery - Delivery location - Vehicle registration (if applicable) - Item quantities and condition ## Reversibility There is no "unissue" action. Once issued, the delivery note remains in issued status until: - You cancel it (→ `cancelled`) - You convert it to an invoice (→ `converted`) If issued by mistake: 1. Cannot revert to draft 2. Must cancel if delivery didn't occur 3. Create new delivery note if needed ## Integration Points ### Logistics Systems After issuing: - Update logistics system with delivery confirmation - Close transport order - Update driver route status - Record delivery time ### Inventory Systems After issuing: - Deduct items from warehouse inventory - Update stock levels - Record location of goods (transferred to client) - Trigger reorder if needed ### Accounting Systems After issuing: - Mark goods as delivered (revenue recognition) - Prepare for invoicing - Update client balance (if accrual accounting) ## Best Practices 1. **Issue at delivery time** - Mark as issued when delivery actually occurs 2. **Verify data first** - Review all details before issuing 3. **Capture signatures** - Get client signature on physical copy 4. **Photo evidence** - Take photo of delivered goods and signature 5. **Log the action** - Record who issued it and when 6. **Update systems** - Sync status to logistics and inventory systems 7. **Convert promptly** - Don't delay invoicing after delivery 8. **Archive documents** - Keep signed copies per legal requirements 9. **Notify stakeholders** - Alert accounting and sales teams 10. **Track conversions** - Monitor which delivery notes need invoicing ## Delivery Note Lifecycle ``` draft → issued → converted ↓ cancelled ``` - **Draft** - Created but not yet delivered - **Issued** - Delivery occurred, waiting for invoicing - **Converted** - Invoice created from delivery note - **Cancelled** - Delivery cancelled (from draft or issued) ## Compliance Notes ### Romanian Requirements - Delivery notes (AVZ - Aviz de însoțire a mărfii) required for: - Transport of goods between locations - Deliveries to clients - Proof of delivery for VAT purposes ### Retention Period - Keep signed delivery notes for minimum legal period - Typically 5-10 years depending on regulation - Both physical and digital copies recommended ### Tax Implications - Delivery note date may affect revenue recognition - Must match or precede invoice date - Required for VAT deduction by client ## After Issuing Next steps after marking as issued: 1. Archive signed physical copy 2. Update inventory systems 3. Schedule invoice conversion 4. Monitor payment timeline 5. Track outstanding deliveries not yet invoiced --- ## List Delivery Notes > Retrieve a paginated list of delivery notes with optional filtering URL: https://docs.storno.ro/api-reference/delivery-notes/list # List Delivery Notes Retrieves a paginated list of delivery notes for the authenticated company. Delivery notes document the physical delivery of goods or completion of services, and can later be converted into invoices. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Query Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `page` | integer | No | Page number (default: 1) | | `limit` | integer | No | Items per page (default: 20, max: 100) | | `search` | string | No | Search term for delivery note number or client name | | `status` | string | No | Filter by status: `draft`, `issued`, `converted`, `cancelled` | | `from` | string | No | Start date filter (ISO 8601 format: YYYY-MM-DD) | | `to` | string | No | End date filter (ISO 8601 format: YYYY-MM-DD) | | `clientId` | string | No | Filter by client UUID | ## Request ```bash {% title="cURL" %} curl -X GET "https://api.storno.ro/api/v1/delivery-notes?page=1&limit=20&status=issued" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes?page=1&limit=20&status=issued', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response ```json { "data": [ { "uuid": "950e8400-e29b-41d4-a716-446655440000", "number": "DN-2026-012", "seriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "DN", "nextNumber": 13, "prefix": "DN-", "year": 2026 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "address": "Str. Exemplu 123, București" }, "status": "issued", "issueDate": "2026-02-18", "dueDate": "2026-03-18", "currency": "RON", "exchangeRate": 1.0, "subtotal": "5000.00", "vatAmount": "950.00", "total": "5950.00", "deliveryLocation": "Client warehouse - Str. Depozit 5, București", "projectReference": "PROJECT-2026-002", "issuerName": "John Doe", "deputyName": "Jane Smith", "deputyIdentityCard": "AB123456", "deputyAuto": "B-123-ABC", "notes": "Handle with care - fragile items", "issuedAt": "2026-02-18T14:30:00Z", "convertedAt": null, "convertedInvoiceId": null, "createdAt": "2026-02-18T09:00:00Z", "updatedAt": "2026-02-18T14:30:00Z" } ], "total": 35, "page": 1, "limit": 20, "pages": 2 } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `data` | array | Array of delivery note objects | | `total` | integer | Total number of delivery notes matching the filters | | `page` | integer | Current page number | | `limit` | integer | Items per page | | `pages` | integer | Total number of pages | ### Delivery Note Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `number` | string | Delivery note number (e.g., DN-2026-012) | | `status` | string | Status: `draft`, `issued`, `converted`, `cancelled` | | `issueDate` | string | Date of issue (YYYY-MM-DD) | | `dueDate` | string | Due date for invoicing (YYYY-MM-DD) | | `currency` | string | Currency code | | `exchangeRate` | number | Exchange rate to company base currency | | `subtotal` | string | Subtotal amount (excluding VAT) | | `vatAmount` | string | Total VAT amount | | `total` | string | Total amount (including VAT) | | `deliveryLocation` | string | Where goods were delivered | | `projectReference` | string | Related project reference | | `deputyName` | string | Name of person who received delivery | | `deputyIdentityCard` | string | ID card number of deputy | | `deputyAuto` | string | Vehicle registration number | | `client` | object | Client details | | `series` | object | Series details | | `issuedAt` | string \| null | ISO 8601 timestamp when issued | | `convertedAt` | string \| null | ISO 8601 timestamp when converted to invoice | | `convertedInvoiceId` | string \| null | UUID of the created invoice (if converted) | ## Status Lifecycle Delivery notes follow this status flow: - **draft** → Initial state when created - **issued** → Issued and delivered to client - **converted** → Converted to an invoice - **cancelled** → Delivery note was cancelled Once a delivery note is `converted` or `cancelled`, it cannot be modified. ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 422 | `validation_error` | Invalid query parameters (e.g., invalid status value, invalid date format) | | 500 | `internal_error` | Server error occurred | ## Use Cases ### Goods Delivery Tracking Document physical delivery of products: - Record what was delivered - Who received it (deputy information) - When and where delivery occurred - Vehicle used for transport ### Service Completion Documentation Document completion of services: - Services performed - Location where services delivered - Person who verified completion - Date of completion ### Pre-Invoice Documentation Create delivery notes before invoicing: 1. Issue delivery note upon delivery/completion 2. Client verifies and accepts delivery 3. Convert to invoice for payment ### Batch Invoicing Accumulate multiple delivery notes: 1. Issue delivery notes throughout the month 2. Collect all delivery notes for a client 3. Create single invoice covering all deliveries ## Best Practices 1. **Issue promptly** - Create delivery note at time of delivery 2. **Record deputy details** - Always capture who received delivery 3. **Track conversion** - Monitor which delivery notes need invoicing 4. **Batch strategically** - Group related deliveries for efficient invoicing 5. **Clear locations** - Specify exact delivery addresses 6. **Vehicle tracking** - Record transport vehicle for logistics 7. **Client signatures** - Keep signed copies as proof of delivery 8. **Convert regularly** - Don't let delivery notes age without invoicing --- ## Restore Delivery Note > Restore a cancelled delivery note back to draft status URL: https://docs.storno.ro/api-reference/delivery-notes/restore # Restore Delivery Note Restores a cancelled delivery note back to `draft` status. Use this endpoint to undo an accidental cancellation. The delivery note can then be edited and re-issued. Only delivery notes in `cancelled` status can be restored. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the delivery note to restore | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/restore \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/restore', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response Returns the restored delivery note with `status = draft` and `cancelledAt` cleared: ```json { "uuid": "950e8400-e29b-41d4-a716-446655440000", "number": "DN-2026-012", "seriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "DN", "nextNumber": 13 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "email": "contact@client.ro" }, "status": "draft", "issueDate": "2026-02-18", "dueDate": "2026-03-18", "currency": "RON", "exchangeRate": 1.0, "subtotal": "5000.00", "vatAmount": "950.00", "total": "5950.00", "cancellationReason": null, "cancellationNotes": null, "issuedAt": null, "cancelledAt": null, "createdAt": "2026-02-18T09:00:00Z", "updatedAt": "2026-02-19T10:00:00Z" } ``` ## State Changes - **Status:** Changed from `cancelled` → `draft` - **cancelledAt:** Cleared (set to `null`) - **cancellationReason:** Cleared (set to `null`) - **cancellationNotes:** Cleared (set to `null`) - **updatedAt:** Updated to current UTC timestamp After restoring, the delivery note can be edited, re-issued, or deleted. ## Validation Rules - Delivery note must be in `cancelled` status - Cannot restore a delivery note that is `draft`, `issued`, or `converted` ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Delivery note not found or doesn't belong to the company | | 422 | `validation_error` | Delivery note is not in cancelled status | | 500 | `internal_error` | Server error occurred | ## Example Error Response ### Not Cancelled ```json { "error": { "code": "validation_error", "message": "Delivery note cannot be restored", "details": { "status": "draft", "reason": "Only cancelled delivery notes can be restored" } } } ``` ## Workflow Integration ### Restore Flow 1. Identify accidentally cancelled delivery note 2. **Call restore endpoint** (`POST /api/v1/delivery-notes/{uuid}/restore`) ← You are here 3. Optionally edit the delivery note if needed 4. Re-issue the delivery note (`POST /api/v1/delivery-notes/{uuid}/issue`) ## Related Endpoints - [Cancel delivery note](/api-reference/delivery-notes/cancel) - Cancel a delivery note - [Issue delivery note](/api-reference/delivery-notes/issue) - Issue the restored draft - [Update delivery note](/api-reference/delivery-notes/update) - Edit the restored draft --- ## Send Delivery Note Email > Send a delivery note to a client via email with PDF attachment URL: https://docs.storno.ro/api-reference/delivery-notes/email # Send Delivery Note Email Sends a delivery note to a client via email with the PDF attached. The subject and body can be customized or left blank to use the company's default email template. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the delivery note to email | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `to` | string | Yes | Recipient email address | | `subject` | string | No | Email subject (uses default template if omitted) | | `body` | string | No | Email body (uses default template if omitted) | | `cc` | string[] | No | Array of CC email addresses | | `bcc` | string[] | No | Array of BCC email addresses | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/email \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "to": "contact@client.ro", "subject": "Aviz de livrare DN-2026-012", "body": "Buna ziua,\n\nGasiti atasat avizul de livrare DN-2026-012.\n\nMultumim!", "cc": ["accounting@client.ro"] }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/email', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ to: 'contact@client.ro', subject: 'Aviz de livrare DN-2026-012', body: 'Buna ziua,\n\nGasiti atasat avizul de livrare DN-2026-012.\n\nMultumim!', cc: ['accounting@client.ro'] }) }); const data = await response.json(); ``` ## Response Returns an email log object confirming the email was sent: ```json { "id": "8f9a0b1c-2d3e-4f5a-6b7c-8d9e0f1a2b3c", "to": "contact@client.ro", "cc": ["accounting@client.ro"], "bcc": null, "subject": "Aviz de livrare DN-2026-012", "attachments": ["aviz-DN-2026-012.pdf"], "sentAt": "2026-02-18T14:35:00Z", "deliveryStatus": "sent" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Email log UUID | | `to` | string | Primary recipient email address | | `cc` | string[]\|null | CC recipients | | `bcc` | string[]\|null | BCC recipients | | `subject` | string | Subject line that was sent | | `attachments` | string[] | List of attached file names | | `sentAt` | string | ISO 8601 timestamp of when the email was sent | | `deliveryStatus` | string | `sent`, `queued`, or `failed` | ## Template Variables When using the default email template (subject or body omitted), the following variables are substituted automatically: | Variable | Description | Example | |----------|-------------|---------| | `[[client_name]]` | Client company name | Client SRL | | `[[delivery_note_number]]` | Delivery note number | DN-2026-012 | | `[[total]]` | Formatted total with currency | 5,950.00 RON | | `[[issue_date]]` | Issue date | 18.02.2026 | | `[[company_name]]` | Your company name | Your Company SRL | | `[[currency]]` | Currency code | RON | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid or missing `to` email address | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Delivery note not found or doesn't belong to the company | | 500 | `internal_error` | Email sending failed | ## Related Endpoints - [Get email defaults](/api-reference/delivery-notes/email-defaults) - Get pre-filled subject and body - [Get email history](/api-reference/delivery-notes/email-history) - View previously sent emails - [Download PDF](/api-reference/delivery-notes/pdf) - Download the PDF directly --- ## Submit Delivery Note to e-Transport > Submit an issued delivery note to ANAF's e-Transport system URL: https://docs.storno.ro/api-reference/delivery-notes/submit-etransport # Submit Delivery Note to e-Transport Submits an issued delivery note to Romania's ANAF e-Transport system to generate a TTN (Tracked Transport Number) declaration for domestic transport. The submission is asynchronous. The API call immediately sets `etransportStatus` to `uploaded` and dispatches a background job that validates the delivery note, generates the required XML, uploads it to ANAF, and polls for a UIT (Unique Identification of Transport). The final status is updated once ANAF processes the declaration. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the delivery note to submit | ## Request No request body required. ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/submit-etransport \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000/submit-etransport', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response Returns the updated delivery note with `etransportStatus = uploaded` and `etransportSubmittedAt` set to the current timestamp: ```json { "uuid": "950e8400-e29b-41d4-a716-446655440000", "number": "DN-2026-012", "seriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "DN", "nextNumber": 13 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "email": "contact@client.ro" }, "status": "issued", "issueDate": "2026-02-18", "currency": "RON", "exchangeRate": 1.0, "subtotal": "5000.00", "vatAmount": "950.00", "total": "5950.00", "etransportStatus": "uploaded", "etransportSubmittedAt": "2026-02-18T14:35:00Z", "etransportUit": null, "issuedAt": "2026-02-18T14:30:00Z", "createdAt": "2026-02-18T09:00:00Z", "updatedAt": "2026-02-18T14:35:00Z" } ``` ## State Changes ### etransportStatus Transitions | Transition | When | |------------|------| | `null` → `uploaded` | Immediately on successful API call | | `uploaded` → `ok` | ANAF accepted the declaration and returned a UIT | | `uploaded` → `nok` | ANAF rejected the declaration | | `null` → `validation_failed` | Entity validation failed before upload | | `null` → `upload_failed` | ANAF API error during upload | ### Fields Updated - `etransportStatus` — set to `uploaded` on the synchronous response - `etransportSubmittedAt` — set to current UTC timestamp (ISO 8601) - `etransportUit` — populated with the UIT string once ANAF returns `ok` - `updatedAt` — updated on every status change ## Validation Rules ### Status Requirement - The delivery note must be in `issued` status. - Delivery notes in `draft`, `cancelled`, or `converted` status cannot be submitted. ### e-Transport Fields Required The delivery note must have all mandatory e-Transport fields populated before submission: - Vehicle registration number - Transport route (loading and unloading addresses) - Declared transport date - Tariff codes (NC codes) on all lines - Gross weight per line If any required field is missing, the background job sets `etransportStatus` to `validation_failed` without contacting ANAF. ### Re-submission A delivery note with `etransportStatus` of `ok` (already has a valid UIT) cannot be resubmitted. Delivery notes with `nok`, `validation_failed`, or `upload_failed` status can be corrected and resubmitted. ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Delivery note not found or doesn't belong to the company | | 422 | `validation_error` | Delivery note is not in `issued` status, or already has a valid UIT | ## Best Practices 1. **Check status before submitting** — Confirm `status = issued` and `etransportStatus` is not `ok` before calling this endpoint. 2. **Poll for final status** — After receiving `uploaded`, poll the delivery note (or listen for a webhook) until `etransportStatus` transitions to `ok`, `nok`, `validation_failed`, or `upload_failed`. 3. **Fill all e-Transport fields first** — Ensure vehicle number, route, NC tariff codes, and weights are complete before submitting to avoid `validation_failed`. 4. **Handle nok gracefully** — If ANAF returns `nok`, review the error details on the delivery note, correct the data, and resubmit. 5. **Store the UIT** — Once `etransportStatus = ok`, persist the `etransportUit` value; it is required to accompany the physical transport. --- ## Update Delivery Note > Update an existing delivery note (draft or issued status) URL: https://docs.storno.ro/api-reference/delivery-notes/update # Update Delivery Note Updates an existing delivery note. Delivery notes in both `draft` and `issued` status can be updated. Once converted or cancelled, the delivery note becomes immutable. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the delivery note to update | ## Request Body All fields from the create endpoint can be updated. The entire delivery note is replaced with the new data. | Field | Type | Required | Description | |-------|------|----------|-------------| | `clientId` | string | Yes | UUID of the client | | `documentSeriesId` | string | No | UUID of the delivery note series. Can be changed to any active `delivery_note` series; if omitted, the existing series is preserved | | `issueDate` | string | Yes | Date of issue (YYYY-MM-DD) | | `dueDate` | string | Yes | Due date for invoicing (YYYY-MM-DD) | | `currency` | string | Yes | Currency code | | `exchangeRate` | number | No | Exchange rate | | `deliveryLocation` | string | No | Delivery address | | `projectReference` | string | No | Project reference | | `issuerName` | string | No | Issuer name | | `issuerId` | string | No | Issuer UUID | | `salesAgent` | string | No | Sales agent name | | `deputyName` | string | No | Deputy name | | `deputyIdentityCard` | string | No | Deputy ID card | | `deputyAuto` | string | No | Vehicle registration | | `notes` | string | No | Public notes | | `mentions` | string | No | Additional mentions | | `internalNote` | string | No | Internal note | | `lines` | array | Yes | Array of line items (replaces all existing lines) | ### e-Transport Fields All e-Transport fields from the [Create endpoint](/api-reference/delivery-notes/create#e-transport-fields) are accepted. Key fields: | Field | Type | Required | Description | |-------|------|----------|-------------| | `etransportOperationType` | number | No | Operation type (10, 12, 20, 30, 40, 50, 60) | | `etransportVehicleNumber` | string | No | Vehicle registration (3-20 uppercase alphanumeric) | | `etransportTransporterCountry` | string | No | Transporter country code (e.g., "RO") | | `etransportTransporterCode` | string | No | Transporter CUI/CIF (numeric only) | | `etransportTransporterName` | string | No | Transporter legal name | | `etransportTransportDate` | string | No | Transport start date (YYYY-MM-DD) | | `etransportPostIncident` | boolean | No | Post-incident declaration | | `etransportTrailer1` | string | No | First trailer registration | | `etransportTrailer2` | string | No | Second trailer registration | | `etransportStartCounty` | number | No | Start county code (1-52) | | `etransportStartLocality` | string | No | Start locality (2-100 chars) | | `etransportStartStreet` | string | No | Start street (2-100 chars) | | `etransportStartNumber` | string | No | Start street number | | `etransportStartOtherInfo` | string | No | Start additional info | | `etransportStartPostalCode` | string | No | Start postal code | | `etransportEndCounty` | number | No | End county code (1-52) | | `etransportEndLocality` | string | No | End locality (2-100 chars) | | `etransportEndStreet` | string | No | End street (2-100 chars) | | `etransportEndNumber` | string | No | End street number | | `etransportEndOtherInfo` | string | No | End additional info | | `etransportEndPostalCode` | string | No | End postal code | ### Line Item Object | Field | Type | Required | Description | |-------|------|----------|-------------| | `uuid` | string | No | UUID of existing line (if updating); omit to create new line | | `description` | string | Yes | Item description | | `quantity` | number | Yes | Quantity (must be > 0) | | `unitPrice` | number | Yes | Price per unit | | `vatRateId` | string | Yes | UUID of the VAT rate | | `unitOfMeasure` | string | No | Unit of measure | | `productId` | string | No | UUID of related product | | `tariffCode` | string | No | 8-digit HS/CN tariff code (required for e-Transport, BR-206) | | `purposeCode` | number | No | Purpose code (for TTN: 101, 704, 705, 9901 per BR-070) | | `unitOfMeasureCode` | string | No | UN/ECE Rec 20 code (e.g., "H87", "KGM", "SET") | | `netWeight` | string | No | Net weight in kg (required for e-Transport, BR-207) | | `grossWeight` | string | No | Gross weight in kg | | `valueWithoutVat` | string | No | Value without VAT (required for e-Transport, BR-208) | ## Request ```bash {% title="cURL" %} curl -X PUT https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "clientId": "750e8400-e29b-41d4-a716-446655440000", "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "issueDate": "2026-02-18", "dueDate": "2026-03-20", "currency": "RON", "deliveryLocation": "Updated: Client new warehouse - Str. Nou 10", "projectReference": "PROJECT-2026-002", "issuerName": "John Doe", "deputyName": "Updated Deputy Name", "deputyIdentityCard": "CD789012", "deputyAuto": "B-456-XYZ", "notes": "Updated delivery notes", "lines": [ { "uuid": "A10e8400-e29b-41d4-a716-446655440000", "description": "Laptop Dell Latitude 7420 (Updated qty)", "quantity": 12, "unitPrice": 450, "unitOfMeasure": "piece", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "B50e8400-e29b-41d4-a716-446655440000" }, { "description": "USB-C Cable 2m", "quantity": 12, "unitPrice": 15, "unitOfMeasure": "piece", "vatRateId": "350e8400-e29b-41d4-a716-446655440000" } ] }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/delivery-notes/950e8400-e29b-41d4-a716-446655440000', { method: 'PUT', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: '750e8400-e29b-41d4-a716-446655440000', documentSeriesId: '850e8400-e29b-41d4-a716-446655440000', issueDate: '2026-02-18', dueDate: '2026-03-20', currency: 'RON', deliveryLocation: 'Updated: Client new warehouse - Str. Nou 10', projectReference: 'PROJECT-2026-002', issuerName: 'John Doe', deputyName: 'Updated Deputy Name', deputyIdentityCard: 'CD789012', deputyAuto: 'B-456-XYZ', notes: 'Updated delivery notes', lines: [ { uuid: 'A10e8400-e29b-41d4-a716-446655440000', description: 'Laptop Dell Latitude 7420 (Updated qty)', quantity: 12, unitPrice: 450, unitOfMeasure: 'piece', vatRateId: '350e8400-e29b-41d4-a716-446655440000', productId: 'B50e8400-e29b-41d4-a716-446655440000' }, { description: 'USB-C Cable 2m', quantity: 12, unitPrice: 15, unitOfMeasure: 'piece', vatRateId: '350e8400-e29b-41d4-a716-446655440000' } ] }) }); const data = await response.json(); ``` ## Response Returns the updated delivery note object with recalculated totals: ```json { "uuid": "950e8400-e29b-41d4-a716-446655440000", "number": "DN-2026-012", "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "clientId": "750e8400-e29b-41d4-a716-446655440000", "status": "draft", "issueDate": "2026-02-18", "dueDate": "2026-03-20", "currency": "RON", "exchangeRate": 1.0, "deliveryLocation": "Updated: Client new warehouse - Str. Nou 10", "projectReference": "PROJECT-2026-002", "issuerName": "John Doe", "deputyName": "Updated Deputy Name", "deputyIdentityCard": "CD789012", "deputyAuto": "B-456-XYZ", "notes": "Updated delivery notes", "lines": [ { "uuid": "A10e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Laptop Dell Latitude 7420 (Updated qty)", "quantity": "12.00", "unitPrice": "450.00", "unitOfMeasure": "piece", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "B50e8400-e29b-41d4-a716-446655440000", "subtotal": "5400.00", "vatAmount": "1026.00", "total": "6426.00" }, { "uuid": "A30e8400-e29b-41d4-a716-446655440000", "lineNumber": 2, "description": "USB-C Cable 2m", "quantity": "12.00", "unitPrice": "15.00", "unitOfMeasure": "piece", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "subtotal": "180.00", "vatAmount": "34.20", "total": "214.20" } ], "subtotal": "5580.00", "vatAmount": "1060.20", "total": "6640.20", "createdAt": "2026-02-18T09:00:00Z", "updatedAt": "2026-02-18T11:30:00Z" } ``` ## Line Item Behavior When updating lines: - Lines with existing `uuid` values are updated - Lines without `uuid` are created as new lines - Existing lines not included in the request are **deleted** - Line numbers are automatically reassigned sequentially ## Validation Rules ### Status Restriction - Delivery note must be in `draft` or `issued` status - Cannot update after it's converted or cancelled ### Dates - `issueDate` must be valid YYYY-MM-DD format - `dueDate` must be equal to or after `issueDate` ### Currency & Rates - `currency` must be valid ISO 4217 code - `exchangeRate` must be greater than 0 ### Line Items - Minimum 1 line required - All line validation rules from create endpoint apply ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid request body structure | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header, or delivery note is not editable | | 404 | `not_found` | Delivery note not found or doesn't belong to the company | | 409 | `conflict` | Delivery note status prevents updates (converted or cancelled) | | 422 | `validation_error` | Validation failed (see error details) | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict ```json { "error": { "code": "conflict", "message": "Delivery note cannot be updated", "details": { "status": "converted", "reason": "Converted or cancelled delivery notes cannot be updated", "convertedAt": "2026-02-20T15:00:00Z" } } } ``` ### Validation Error ```json { "error": { "code": "validation_error", "message": "Validation failed", "details": { "dueDate": ["Due date must be after issue date"], "lines.0.quantity": ["Quantity must be greater than 0"], "lines": ["At least one line item is required"] } } } ``` ## Common Update Scenarios ### Update Quantities Before Delivery ```javascript // Adjust quantities to match actual stock lines: [ { uuid: "A10...", quantity: 12, ... }, // Was 10, now 12 { uuid: "A20...", quantity: 10, ... } // Unchanged ] ``` ### Add Items to Delivery ```javascript // Keep existing + add new items lines: [ { uuid: "A10...", description: "Item 1", ... }, // Keep existing { description: "New Item", quantity: 5, ... } // Add new ] ``` ### Remove Items from Delivery ```javascript // Only include items being delivered lines: [ { uuid: "A10...", description: "Item 1", ... } // Keep only this // Item 2 removed by omitting it ] ``` ### Update Delivery Details ```javascript // Change delivery location or deputy { deliveryLocation: "New warehouse address", deputyName: "Different person receiving", deputyIdentityCard: "New ID number" } ``` ### Correct Data Entry Errors ```javascript // Fix mistakes before issuing { projectReference: "Corrected project code", notes: "Corrected delivery notes", lines: [{ ...corrected line data }] } ``` ## Best Practices 1. **Update before issuing** - Make all corrections while in draft status 2. **Verify totals** - Check calculated totals after updating quantities 3. **Track changes** - Log what was changed and why in your system 4. **Confirm with client** - If delivery details change, notify client 5. **Update promptly** - Make changes as soon as discrepancies are discovered 6. **Check inventory** - Ensure updated quantities match available stock 7. **Issue when ready** - Once finalized, mark as issued immediately ## When to Update vs Create New ### Update Existing - Delivery note is in `draft` or `issued` status - Fixing errors before or after issue - Adjusting quantities to match stock - Correcting client or location details ### Create New Delivery Note - Need to document an additional, separate delivery - Delivery note is already converted or cancelled - Separate shipment or delivery event - Historical record of the original must remain unchanged ## Next Steps After updating a delivery note: 1. Review all changes carefully 2. Verify totals and calculations 3. Confirm inventory availability 4. Mark as issued when ready for delivery 5. Convert to invoice after successful delivery --- ## Register Device > Register device token for push notifications URL: https://docs.storno.ro/api-reference/devices/register # Register Device Register a device token for push notifications. --- ## Register Device Token ```http POST /api/v1/devices ``` Register a device token to enable push notifications for the authenticated user. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | | Content-Type | string | Yes | Must be `application/json` | ### Request Body ```json { "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]", "platform": "ios" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | token | string | Yes | Device push notification token | | platform | string | Yes | Device platform: `ios`, `android`, or `web` | ### Response Returns `201 Created` with the device registration: ```json { "id": "bb0e8400-e29b-41d4-a716-446655440007", "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]", "platform": "ios", "registeredAt": "2026-02-16T12:00:00Z" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | id | string | Device registration UUID | | token | string | Device push token | | platform | string | Device platform | | registeredAt | string | ISO 8601 registration timestamp | ### Platform Values | Platform | Description | Token Format | |----------|-------------|--------------| | ios | iOS devices | Apple APNs token or Expo token | | android | Android devices | Firebase FCM token or Expo token | | web | Web browsers | Web Push subscription or Expo token | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 422 | Validation error - invalid token or platform | ### Notes - Multiple devices can be registered per user - Registering the same token again updates the existing registration - Tokens are validated for format based on platform - Use the `/devices` DELETE endpoint to unregister - Expo push tokens work across all platforms - Only active devices receive push notifications --- ## Unregister Device > Unregister device token from push notifications URL: https://docs.storno.ro/api-reference/devices/unregister # Unregister Device Unregister a device token to stop receiving push notifications. --- ## Unregister Device Token ```http DELETE /api/v1/devices ``` Unregister a device token. The device will no longer receive push notifications. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | | Content-Type | string | Yes | Must be `application/json` | ### Request Body ```json { "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | token | string | Yes | Device push notification token to unregister | ### Response Returns `204 No Content` on successful unregistration. ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 404 | Device token not found | ### Notes - Unregistering an already-unregistered token returns success - This operation is idempotent - Call this when user logs out or disables notifications - The same token can be registered again later - Inactive tokens are automatically cleaned up after 90 days --- ## Create document series > Create a new document series for invoices, proformas, credit notes, or delivery notes. URL: https://docs.storno.ro/api-reference/document-series/create # Create document series Creates a new document series for the authenticated company. The series prefix must be unique per company and document type. ```http POST /api/v1/document-series ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `prefix` | string | Yes | Series prefix (unique per company+type) | | `type` | string | Yes | Document type: `invoice`, `proforma`, `credit_note`, `delivery_note` | | `currentNumber` | integer | No | Starting number (default: 0) | | `active` | boolean | No | Whether series is active (default: true) | ## Response Returns the created document series object with a `201 Created` status. ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/document-series' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "prefix": "FAC2026", "type": "invoice", "currentNumber": 0, "active": true }' ``` ## Example Response ```json { "uuid": "series-uuid-4", "prefix": "FAC2026", "type": "invoice", "currentNumber": 0, "nextNumber": 1, "active": true, "createdAt": "2026-02-16T16:00:00Z", "updatedAt": "2026-02-16T16:00:00Z" } ``` ## Prefix Guidelines - Should be short and meaningful (e.g., "FAC", "PRO", "AVZ") - Can include year for annual series (e.g., "FAC2026") - Must be unique per company and document type - Typically uppercase, but no strict format requirement - Avoid special characters that may cause issues in file names ## Common Use Cases ### Fiscal Year Series Create a new series for each fiscal year: - "FAC2025" for 2025 invoices - "FAC2026" for 2026 invoices ### Department Series Create separate series by department: - "FAC-IT" for IT department - "FAC-HR" for HR department ### Document Type Series Different prefixes for different invoice types: - "FAC" for regular invoices - "FRE" for recurring invoices - "FEX" for export invoices ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - Missing `prefix` field - Missing `type` field - Invalid `type` value - Prefix already exists for this company and type - Negative `currentNumber` ## Related Endpoints - [List document series](/api-reference/document-series/list) - [Update document series](/api-reference/document-series/update) - [Delete document series](/api-reference/document-series/delete) --- ## Delete document series > Permanently delete a document series. URL: https://docs.storno.ro/api-reference/document-series/delete # Delete document series Permanently deletes a document series from the authenticated company. ```http DELETE /api/v1/document-series/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the document series | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns a `204 No Content` status on successful deletion with no response body. ## Example Request ```bash curl -X DELETE 'https://api.storno.ro/api/v1/document-series/series-uuid-4' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```http HTTP/1.1 204 No Content ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Document series not found or doesn't belong to company | | 409 | conflict | Cannot delete series with existing documents | ## Important Notes - This is a permanent delete operation - data cannot be recovered - You cannot delete a series that has been used for any documents - Existing invoices that reference this series will retain the series prefix in their stored data - Consider marking the series as inactive instead of deleting it ## Recommended Approach Instead of deleting a series, consider: 1. **Mark as inactive** - Use the [update endpoint](/api-reference/document-series/update) to set `active: false` 2. **Preserve history** - Inactive series maintain referential integrity with existing documents 3. **Audit trail** - Keeping inactive series provides a complete audit trail ## Use Cases for Deletion Deletion should only be used when: - The series was created by mistake and has never been used - Testing or development series that need cleanup - Duplicate series that were never activated ## Related Endpoints - [List document series](/api-reference/document-series/list) - [Create document series](/api-reference/document-series/create) - [Update document series](/api-reference/document-series/update) --- ## List document series > Retrieve all document series for the authenticated company. URL: https://docs.storno.ro/api-reference/document-series/list # List document series Retrieves all document series configured for the authenticated company. Results can be filtered by document type. ```http GET /api/v1/document-series ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `type` | string | No | Filter by document type: `invoice`, `proforma`, `credit_note`, `delivery_note` | ## Response Returns an array of document series objects. ### Document Series Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `prefix` | string | Series prefix (e.g., "FAC", "PRO") | | `type` | string | Document type: `invoice`, `proforma`, `credit_note`, `delivery_note` | | `currentNumber` | integer | Last used number in series | | `nextNumber` | integer | Next available number (virtual field) | | `active` | boolean | Whether series is active | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/document-series?type=invoice' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json [ { "uuid": "series-uuid-1", "prefix": "FAC", "type": "invoice", "currentNumber": 245, "nextNumber": 246, "active": true, "createdAt": "2025-01-01T08:00:00Z", "updatedAt": "2026-02-16T10:30:00Z" }, { "uuid": "series-uuid-2", "prefix": "FRE", "type": "invoice", "currentNumber": 123, "nextNumber": 124, "active": true, "createdAt": "2025-06-15T09:00:00Z", "updatedAt": "2026-02-10T14:20:00Z" }, { "uuid": "series-uuid-3", "prefix": "FAC2025", "type": "invoice", "currentNumber": 1523, "nextNumber": 1524, "active": false, "createdAt": "2025-01-01T08:00:00Z", "updatedAt": "2025-12-31T23:59:00Z" } ] ``` ## Document Types - `invoice` - Standard invoices - `proforma` - Proforma invoices - `credit_note` - Credit notes - `delivery_note` - Delivery notes ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 422 | validation_error | Invalid type parameter | ## Important Notes - Each prefix must be unique per company and document type - The `nextNumber` field is a virtual field calculated as `currentNumber + 1` - Only active series are available for new documents - Inactive series are typically used for closed fiscal years or discontinued numbering schemes ## Related Endpoints - [Create document series](/api-reference/document-series/create) - [Update document series](/api-reference/document-series/update) - [Delete document series](/api-reference/document-series/delete) --- ## Update document series > Update an existing document series. URL: https://docs.storno.ro/api-reference/document-series/update # Update document series Updates an existing document series. Note that the prefix and type cannot be changed after creation. ```http PATCH /api/v1/document-series/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the document series | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters All parameters are optional, but at least one must be provided. | Parameter | Type | Description | |-----------|------|-------------| | `currentNumber` | integer | Update the last used number (use with caution) | | `active` | boolean | Set series as active or inactive | ## Response Returns the updated document series object. ## Example Request ```bash curl -X PATCH 'https://api.storno.ro/api/v1/document-series/series-uuid-3' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "active": false }' ``` ## Example Response ```json { "uuid": "series-uuid-3", "prefix": "FAC2025", "type": "invoice", "currentNumber": 1523, "nextNumber": 1524, "active": false, "createdAt": "2025-01-01T08:00:00Z", "updatedAt": "2026-02-16T16:15:00Z" } ``` ## Field Restrictions ### Non-Editable Fields The following fields cannot be changed after creation: - `prefix` - Series prefix - `type` - Document type These restrictions prevent breaking existing invoice number sequences and ensure data integrity. ### Editable Fields - `currentNumber` - Can be updated, but use with extreme caution - `active` - Can be toggled to activate/deactivate series ## Updating currentNumber Modifying `currentNumber` should only be done in exceptional circumstances: - Correcting data migration issues - Aligning with external accounting systems - Fixing numbering gaps ### Warning Setting `currentNumber` to a value lower than the highest existing invoice number in this series may cause duplicate numbers, which could violate fiscal regulations. ### Best Practice Instead of modifying `currentNumber`, consider: - Creating a new series with the desired starting number - Marking the old series as inactive ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Document series not found or doesn't belong to company | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - No fields provided for update - Negative `currentNumber` - Attempting to modify `prefix` or `type` ## Related Endpoints - [List document series](/api-reference/document-series/list) - [Create document series](/api-reference/document-series/create) - [Delete document series](/api-reference/document-series/delete) --- ## Get E-Factura Message > Get detailed information about a specific e-Factura message URL: https://docs.storno.ro/api-reference/efactura-messages/get # Get E-Factura Message Retrieve detailed information about a specific e-Factura message. --- ## Get Message Details ```http GET /api/v1/efactura-messages/{uuid} ``` Get full details of an e-Factura message including all metadata and related invoice information. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | uuid | string | Message UUID | ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response ```json { "uuid": "990e8400-e29b-41d4-a716-446655440005", "invoiceId": "123e4567-e89b-12d3-a456-426614174000", "invoice": { "number": "FINV2026001", "date": "2026-02-15", "client": "SC Example SRL", "total": 11900.00 }, "messageType": "response", "status": "ok", "messageId": "12345678", "details": { "code": "200", "message": "Factura a fost acceptată de ANAF SPV", "uploadIndex": "987654321", "stateDate": "2026-02-16T11:30:15Z" }, "rawData": { "Titlu": "Factura cu id_incarcare=987654321 emisa de cif_emitent=12345678", "Detalii": "Factura este in curs de procesare", "tip": "FACTURA PRIMITA", "id": "12345678" }, "receivedAt": "2026-02-16T11:30:00Z", "processedAt": "2026-02-16T11:30:15Z" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | uuid | string | Message UUID | | invoiceId | string\|null | Related invoice UUID | | invoice | object\|null | Basic invoice information | | invoice.number | string | Invoice number | | invoice.date | string | Invoice date | | invoice.client | string | Client name | | invoice.total | number | Invoice total amount | | messageType | string | Message type | | status | string | Message status | | messageId | string | ANAF message identifier | | details | object | Parsed message details | | rawData | object | Raw message data from ANAF | | receivedAt | string | ISO 8601 timestamp when received | | processedAt | string\|null | ISO 8601 timestamp when processed | ### Details Field Structure The `details` object structure varies by message type: #### Response Message ```json { "code": "200", "message": "Factura a fost acceptată", "uploadIndex": "987654321", "stateDate": "2026-02-16T11:30:15Z" } ``` #### Error Message ```json { "code": "ERR_XML_001", "message": "Eroare validare XML: CIF invalid", "field": "cif", "expected": "Format: ROXXXXXXXX", "received": "12345678" } ``` #### Notification Message ```json { "code": "ACCEPTED", "message": "Factura a fost acceptată de destinatar", "downloadId": "123456789", "acceptedDate": "2026-02-16T14:00:00Z" } ``` ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - message belongs to another organization | | 404 | Message not found | ### Notes - Full message details include both parsed and raw data - Raw data preserves the original ANAF response format - Invoice information is included when available - Use this endpoint to troubleshoot invoice upload issues - Message history is retained for audit purposes --- ## List E-Factura Messages > List e-Factura messages from ANAF with filtering URL: https://docs.storno.ro/api-reference/efactura-messages/list # List E-Factura Messages Retrieve e-Factura messages from ANAF SPV platform with pagination and filtering. --- ## Get E-Factura Messages ```http GET /api/v1/efactura-messages ``` Get e-Factura messages (responses, notifications, errors) from ANAF with optional filtering. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | page | integer | No | Page number (default: 1) | | limit | integer | No | Items per page (default: 20, max: 100) | | messageType | string | No | Filter by message type | | status | string | No | Filter by status | ### Response ```json { "data": [ { "uuid": "990e8400-e29b-41d4-a716-446655440005", "invoiceId": "123e4567-e89b-12d3-a456-426614174000", "messageType": "response", "status": "ok", "messageId": "12345678", "details": { "code": "200", "message": "Factura a fost acceptată" }, "receivedAt": "2026-02-16T11:30:00Z" }, { "uuid": "aa0e8400-e29b-41d4-a716-446655440006", "invoiceId": "223e4567-e89b-12d3-a456-426614174001", "messageType": "error", "status": "error", "messageId": "12345679", "details": { "code": "ERR_XML_001", "message": "Eroare validare XML: CIF invalid" }, "receivedAt": "2026-02-16T11:35:00Z" } ], "total": 145, "page": 1, "limit": 20, "pages": 8 } ``` ### Response Fields #### Pagination | Field | Type | Description | |-------|------|-------------| | data | array | Array of message objects | | total | integer | Total number of messages | | page | integer | Current page number | | limit | integer | Items per page | | pages | integer | Total number of pages | #### Message Object | Field | Type | Description | |-------|------|-------------| | uuid | string | Message UUID | | invoiceId | string\|null | Related invoice UUID | | messageType | string | Message type (see types below) | | status | string | Message status (see statuses below) | | messageId | string | ANAF message identifier | | details | object | Message-specific details | | receivedAt | string | ISO 8601 timestamp when received | ### Message Types | Type | Description | |------|-------------| | response | ANAF response to uploaded invoice | | notification | Status notification (accepted, rejected) | | error | Error message from ANAF | | warning | Warning about invoice issues | | info | Informational message | ### Message Statuses | Status | Description | |--------|-------------| | ok | Message processed successfully | | error | Error occurred | | warning | Warning issued | | pending | Processing pending | ### Filtering Examples ```http # Get only error messages GET /api/v1/efactura-messages?messageType=error # Get responses with OK status GET /api/v1/efactura-messages?messageType=response&status=ok # Get recent messages (page 1) GET /api/v1/efactura-messages?page=1&limit=50 ``` ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 422 | Invalid query parameters | ### Notes - Messages are synced automatically during e-Factura sync - Each message relates to a specific invoice (when applicable) - Details field structure varies by message type - Messages are retained for audit purposes - Use filters to troubleshoot invoice upload issues --- ## Create email template > Create a new email template for invoice communications. URL: https://docs.storno.ro/api-reference/email-templates/create # Create email template Creates a new email template for the authenticated company. Templates support dynamic variables that are replaced with actual invoice data when emails are sent. ```http POST /api/v1/email-templates ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `name` | string | Yes | Template name for internal reference | | `subject` | string | Yes | Email subject line (supports variables) | | `body` | string | Yes | Email body content (supports variables) | | `isDefault` | boolean | No | Set as default template (default: false) | ## Response Returns the created email template object with a `201 Created` status. ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/email-templates' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "name": "Factura cu reminder plată", "subject": "Reminder: Factura {{invoice_number}} scadentă pe {{due_date}}", "body": "Bună ziua {{client_name}},\n\nVă reamintim că factura {{invoice_number}} în valoare de {{total}} {{currency}} este scadentă pe {{due_date}}.\n\nVă rugăm să efectuați plata cât mai curând posibil pentru a evita penalitățile de întârziere.\n\nDetalii facturã:\n- Număr: {{invoice_number}}\n- Dată emitere: {{issue_date}}\n- Dată scadență: {{due_date}}\n- Total: {{total}} {{currency}}\n\nVă mulțumim pentru colaborare,\n{{company_name}}", "isDefault": false }' ``` ## Example Response ```json { "uuid": "template-uuid-3", "name": "Factura cu reminder plată", "subject": "Reminder: Factura {{invoice_number}} scadentă pe {{due_date}}", "body": "Bună ziua {{client_name}},\n\nVă reamintim că factura {{invoice_number}} în valoare de {{total}} {{currency}} este scadentă pe {{due_date}}.\n\nVă rugăm să efectuați plata cât mai curând posibil pentru a evita penalitățile de întârziere.\n\nDetalii facturã:\n- Număr: {{invoice_number}}\n- Dată emitere: {{issue_date}}\n- Dată scadență: {{due_date}}\n- Total: {{total}} {{currency}}\n\nVă mulțumim pentru colaborare,\n{{company_name}}", "isDefault": false, "createdAt": "2026-02-16T17:00:00Z", "updatedAt": "2026-02-16T17:00:00Z" } ``` ## Available Variables Use these variables in your subject and body. They will be replaced with actual data when emails are sent: | Variable | Description | Example | |----------|-------------|---------| | `{{invoice_number}}` | Full invoice number | FAC00245 | | `{{client_name}}` | Client name | Acme Corporation SRL | | `{{total}}` | Total amount (formatted) | 2,380.00 | | `{{currency}}` | Currency code | RON | | `{{due_date}}` | Due date (formatted) | 15 martie 2026 | | `{{issue_date}}` | Issue date (formatted) | 15 februarie 2026 | | `{{company_name}}` | Your company name | My Company SRL | ## Default Template Behavior - If `isDefault` is `true`, any existing default template will be set to non-default - Only one template can be marked as default - The default template is used when sending invoices without explicitly specifying a template ## Template Use Cases ### Standard Invoice Email Basic template for sending invoices to clients. ### Payment Reminder Template with stronger language for overdue invoices. ### Recurring Invoice Template specifically for automated recurring invoice emails. ### Thank You Email Template sent after payment is received. ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - Missing `name` field - Missing `subject` field - Missing `body` field - Empty subject or body - Subject or body exceeds maximum length ## Related Endpoints - [List email templates](/api-reference/email-templates/list) - [Update email template](/api-reference/email-templates/update) - [Delete email template](/api-reference/email-templates/delete) --- ## Delete email template > Permanently delete an email template. URL: https://docs.storno.ro/api-reference/email-templates/delete # Delete email template Permanently deletes an email template from the authenticated company. ```http DELETE /api/v1/email-templates/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the email template | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns a `204 No Content` status on successful deletion with no response body. ## Example Request ```bash curl -X DELETE 'https://api.storno.ro/api/v1/email-templates/template-uuid-3' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```http HTTP/1.1 204 No Content ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Email template not found or doesn't belong to company | | 409 | conflict | Cannot delete the last template or the default template | ## Important Notes - This is a permanent delete operation - data cannot be recovered - You cannot delete the last remaining email template for a company - You cannot delete the default template; set another template as default first - Emails already sent using this template are not affected - Recurring invoices configured to use this template will fall back to the default template ## Recommended Approach Before deleting a template: 1. **Check usage** - Verify if any recurring invoices use this template 2. **Set new default** - If deleting the default template, set another as default 3. **Keep alternatives** - Ensure at least one other template exists 4. **Archive content** - Save the template content externally if you might need it later ## Related Endpoints - [List email templates](/api-reference/email-templates/list) - [Create email template](/api-reference/email-templates/create) - [Update email template](/api-reference/email-templates/update) --- ## List email templates > Retrieve all email templates for the authenticated company. URL: https://docs.storno.ro/api-reference/email-templates/list # List email templates Retrieves all email templates configured for the authenticated company. If no templates exist, a default template is automatically created. ```http GET /api/v1/email-templates ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns an array of email template objects. ### Email Template Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `name` | string | Template name | | `subject` | string | Email subject line (supports variables) | | `body` | string | Email body content (supports variables) | | `isDefault` | boolean | Whether this is the default template | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/email-templates' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json [ { "uuid": "template-uuid-1", "name": "Factura standard", "subject": "Factura {{invoice_number}} de la {{company_name}}", "body": "Bună ziua,\n\nVă transmitem factura {{invoice_number}} în valoare de {{total}} RON.\n\nData scadență: {{due_date}}\n\nVă mulțumim,\n{{company_name}}", "isDefault": true, "createdAt": "2025-06-01T10:00:00Z", "updatedAt": "2025-06-01T10:00:00Z" }, { "uuid": "template-uuid-2", "name": "Factura cu reminder", "subject": "Reminder: Factura {{invoice_number}} scadentă", "body": "Bună ziua {{client_name}},\n\nVă reamintim că factura {{invoice_number}} în valoare de {{total}} RON este scadentă pe {{due_date}}.\n\nVă rugăm să procesați plata cât mai curând posibil.\n\nMulțumim,\n{{company_name}}", "isDefault": false, "createdAt": "2025-07-15T14:30:00Z", "updatedAt": "2025-12-10T09:20:00Z" } ] ``` ## Available Variables Email templates support the following dynamic variables: | Variable | Description | Example Output | |----------|-------------|----------------| | `{{invoice_number}}` | Full invoice number | FAC00245 | | `{{client_name}}` | Client name | Acme Corporation SRL | | `{{total}}` | Total amount | 2380.00 | | `{{currency}}` | Currency code | RON | | `{{due_date}}` | Due date | 2026-03-15 | | `{{issue_date}}` | Issue date | 2026-02-15 | | `{{company_name}}` | Your company name | My Company SRL | ## Auto-Seeding If a company has no email templates, the system automatically creates a default template on first request with Romanian content suitable for standard invoice emails. ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | ## Important Notes - Each company should have at least one email template - Only one template can be marked as default - The default template is used when sending invoices without specifying a template - Templates can include HTML formatting for richer email appearance - Variables are replaced at send time with actual invoice data ## Related Endpoints - [Create email template](/api-reference/email-templates/create) - [Update email template](/api-reference/email-templates/update) - [Delete email template](/api-reference/email-templates/delete) --- ## Update email template > Update an existing email template. URL: https://docs.storno.ro/api-reference/email-templates/update # Update email template Updates an existing email template for the authenticated company. ```http PATCH /api/v1/email-templates/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the email template | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters All parameters are optional, but at least one must be provided. | Parameter | Type | Description | |-----------|------|-------------| | `name` | string | Template name for internal reference | | `subject` | string | Email subject line (supports variables) | | `body` | string | Email body content (supports variables) | | `isDefault` | boolean | Set as default template | ## Response Returns the updated email template object. ## Example Request ```bash curl -X PATCH 'https://api.storno.ro/api/v1/email-templates/template-uuid-2' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "name": "Factura cu reminder urgent", "subject": "URGENT: Factura {{invoice_number}} restantă", "body": "Bună ziua {{client_name}},\n\nFactura {{invoice_number}} în valoare de {{total}} {{currency}} a depășit termenul de plată ({{due_date}}).\n\nVă rugăm să efectuați plata de urgență pentru a evita suspendarea serviciilor și aplicarea penalităților.\n\nContact urgent: finance@company.ro\n\nMulțumim,\n{{company_name}}" }' ``` ## Example Response ```json { "uuid": "template-uuid-2", "name": "Factura cu reminder urgent", "subject": "URGENT: Factura {{invoice_number}} restantă", "body": "Bună ziua {{client_name}},\n\nFactura {{invoice_number}} în valoare de {{total}} {{currency}} a depășit termenul de plată ({{due_date}}).\n\nVă rugăm să efectuați plata de urgență pentru a evita suspendarea serviciilor și aplicarea penalităților.\n\nContact urgent: finance@company.ro\n\nMulțumim,\n{{company_name}}", "isDefault": false, "createdAt": "2025-07-15T14:30:00Z", "updatedAt": "2026-02-16T17:15:00Z" } ``` ## Available Variables See the [create endpoint](/api-reference/email-templates/create#available-variables) for a complete list of available template variables. ## Default Template Behavior - If `isDefault` is set to `true`, any existing default template will be set to non-default - Only one template can be marked as default - Setting `isDefault` to `false` on the default template requires another template to be set as default first ## Important Notes - Changes to templates only affect future emails - Emails that have already been sent are not modified - Consider versioning template names if you make significant changes (e.g., "Factura standard v2") ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Email template not found or doesn't belong to company | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - No fields provided for update - Empty subject or body - Subject or body exceeds maximum length ## Related Endpoints - [List email templates](/api-reference/email-templates/list) - [Create email template](/api-reference/email-templates/create) - [Delete email template](/api-reference/email-templates/delete) --- ## Currency Conversion > Convert amounts between currencies using BNR rates URL: https://docs.storno.ro/api-reference/exchange-rates/convert # Currency Conversion Convert an amount from one currency to another using current BNR exchange rates. --- ## Convert Currency ```http GET /api/v1/exchange-rates/convert ``` Convert an amount between two currencies using current BNR rates. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | amount | number | Yes | Amount to convert | | from | string | Yes | Source currency code (ISO 4217) | | to | string | Yes | Target currency code (ISO 4217) | ### Example Request ```http GET /api/v1/exchange-rates/convert?amount=100&from=EUR&to=RON ``` ### Response ```json { "amount": 100, "from": "EUR", "to": "RON", "result": 497.50, "rate": 4.9750, "date": "2026-02-16" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | amount | number | Original amount | | from | string | Source currency code | | to | string | Target currency code | | result | number | Converted amount | | rate | number | Exchange rate used | | date | string | Date of rate (YYYY-MM-DD) | ### Conversion Examples ```http # EUR to RON GET /api/v1/exchange-rates/convert?amount=100&from=EUR&to=RON # Result: 497.50 RON # RON to EUR GET /api/v1/exchange-rates/convert?amount=497.50&from=RON&to=EUR # Result: 100.00 EUR # USD to EUR GET /api/v1/exchange-rates/convert?amount=100&from=USD&to=EUR # Result: Uses cross-rate through RON ``` ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 422 | Validation error - missing parameters or unsupported currency | ### Notes - All conversions use BNR rates (updated daily) - For non-RON to non-RON conversions, cross-rates through RON are calculated - Result is rounded to 2 decimal places for standard currencies - RON is treated as base currency (rate = 1) - Rates are updated once daily from BNR --- ## Exchange Rates > Get current BNR exchange rates URL: https://docs.storno.ro/api-reference/exchange-rates/rates # Exchange Rates Retrieve current exchange rates from BNR (Banca Națională a României). --- ## Get Exchange Rates ```http GET /api/v1/exchange-rates ``` Get the latest exchange rates from BNR. Rates are updated daily (typically around 13:00 EET). ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response ```json { "date": "2026-02-16", "rates": { "EUR": 4.9750, "USD": 4.5600, "GBP": 5.8200, "CHF": 5.2100, "JPY": 0.0307, "CAD": 3.2800, "AUD": 2.9400, "HUF": 0.0123, "CZK": 0.1890, "PLN": 1.1200, "SEK": 0.4150, "DKK": 0.6670, "NOK": 0.4280 } } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | date | string | Date of rates in YYYY-MM-DD format | | rates | object | Exchange rates to RON | ### Rate Object Key-value pairs where: - **Key**: ISO 4217 currency code (e.g., `EUR`, `USD`) - **Value**: Exchange rate to 1 RON (number with 4 decimal places) For example, `"EUR": 4.9750` means 1 EUR = 4.9750 RON. ### Supported Currencies Common currencies include: - EUR - Euro - USD - US Dollar - GBP - British Pound - CHF - Swiss Franc - JPY - Japanese Yen - CAD - Canadian Dollar - AUD - Australian Dollar - HUF - Hungarian Forint - CZK - Czech Koruna - PLN - Polish Zloty - SEK - Swedish Krona - DKK - Danish Krone - NOK - Norwegian Krone ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | ### Notes - Rates are cached and updated once daily from BNR - BNR typically publishes rates around 13:00 EET on business days - On weekends and holidays, the last available rates are returned - Rates are used for invoice currency conversion - All rates are expressed as: 1 foreign currency = X RON --- ## Download Export > Download generated export files URL: https://docs.storno.ro/api-reference/exports/download # Download Export Download generated export files (ZIP archives). --- ## Download Export File ```http GET /api/v1/exports/{filename} ``` Download a generated export file. The file is automatically deleted after serving. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | filename | string | Export filename (e.g., `invoices-export-2026-02.zip`) | ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response Returns the file with appropriate headers: ```http Content-Type: application/zip Content-Disposition: attachment; filename="invoices-export-2026-02.zip" ``` The response body contains the binary ZIP file data. ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - file belongs to another user/organization | | 404 | File not found or already downloaded | ### Notes - Export files are single-use and auto-deleted after download - Files are stored temporarily (typically 24 hours) - File access is restricted to the user/organization that generated it - Filenames are typically formatted as `{type}-export-{date}.zip` - Common export types include: - `invoices-export-{YYYY-MM}.zip` - Monthly invoice exports - `vat-report-{YYYY-MM}.zip` - VAT report exports - `clients-export-{YYYY-MM-DD}.zip` - Client data exports - `products-export-{YYYY-MM-DD}.zip` - Product catalog exports ### Export Generation Exports are typically generated via separate endpoints: - Invoice exports: `POST /api/v1/invoices/export` - VAT reports: `POST /api/v1/reports/vat/export` - Client exports: `POST /api/v1/clients/export` These endpoints return a `filename` that can be used with this download endpoint. --- ## Accept Invitation > View and accept organization invitation URL: https://docs.storno.ro/api-reference/invitations/accept # Accept Invitation View invitation details and accept an invitation to join an organization. --- ## Get Invitation Details ```http GET /api/v1/invitations/accept/{token} ``` Get invitation details for display on the invitation acceptance page. This endpoint is public (no authentication required). ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | token | string | Invitation token from email link | ### Response ```json { "email": "newuser@example.com", "organizationName": "Acme Corporation SRL", "role": "ACCOUNTANT", "invitedBy": "John Doe" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | email | string | Email address invited | | organizationName | string | Name of organization | | role | string | Role being offered | | invitedBy | string | Name of user who sent invitation | ### Error Responses | Status | Description | |--------|-------------| | 404 | Invitation not found or already used | | 410 | Gone - invitation has expired | --- ## Accept Invitation ```http POST /api/v1/invitations/accept/{token} ``` Accept the invitation and join the organization. User must be authenticated. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | token | string | Invitation token from email link | ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response Returns `200 OK` with the membership details: ```json { "organization": { "uuid": "440e8400-e29b-41d4-a716-446655440004", "name": "Acme Corporation SRL" }, "role": "ACCOUNTANT", "allowedCompanies": [ "550e8400-e29b-41d4-a716-446655440000" ] } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | organization | object | Organization details | | organization.uuid | string | Organization UUID | | organization.name | string | Organization name | | role | string | Assigned role | | allowedCompanies | array | Initial company access (may be empty) | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - must be logged in | | 404 | Invitation not found or already used | | 409 | Conflict - user is already a member | | 410 | Gone - invitation has expired | | 422 | Validation error - email mismatch | ### Notes - Authenticated user's email must match invitation email - Invitation is consumed after successful acceptance - User gains immediate access to the organization - Initial company access is configured by organization admin after joining - Invitation token can only be used once --- ## Cancel Invitation > Cancel a pending invitation URL: https://docs.storno.ro/api-reference/invitations/cancel # Cancel Invitation Cancel a pending invitation before it is accepted or expires. --- ## Cancel Invitation ```http DELETE /api/v1/invitations/{uuid} ``` Cancel a pending invitation. The invitation token will be invalidated and cannot be used. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | uuid | string | Invitation UUID | ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response Returns `204 No Content` on successful cancellation. ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - insufficient permissions | | 404 | Invitation not found or already accepted | ### Notes - Cannot cancel invitations that have already been accepted - Cancelled invitations cannot be restored - Only organization admins and owners can cancel invitations - User will not be notified of cancellation --- ## Create Invitation > Invite a user to join the organization URL: https://docs.storno.ro/api-reference/invitations/create # Create Invitation Invite a new user to join the organization with a specific role. --- ## Create Invitation ```http POST /api/v1/invitations ``` Send an invitation email to a user to join the organization. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | | Content-Type | string | Yes | Must be `application/json` | ### Request Body ```json { "email": "newuser@example.com", "role": "ACCOUNTANT" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | email | string | Yes | Email address of user to invite | | role | string | Yes | Role: `ADMIN`, `ACCOUNTANT`, or `EMPLOYEE` | ### Response Returns `201 Created` with the invitation object: ```json { "uuid": "770e8400-e29b-41d4-a716-446655440002", "email": "newuser@example.com", "role": "ACCOUNTANT", "createdAt": "2026-02-16T12:00:00Z", "expiresAt": "2026-02-23T12:00:00Z" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | uuid | string | Invitation UUID | | email | string | Invited user email | | role | string | Role assigned to invitation | | createdAt | string | ISO 8601 creation timestamp | | expiresAt | string | ISO 8601 expiry timestamp (7 days) | ### Validation Rules - Email must be valid format - Email cannot already be a member - Role must be one of: `ADMIN`, `ACCOUNTANT`, `EMPLOYEE` - Cannot invite to `OWNER` role ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - insufficient permissions | | 422 | Validation error - invalid email or role, or user already member | ### Notes - Invitations expire after 7 days - An invitation email is automatically sent - User must accept invitation to become a member - Only organization admins and owners can send invitations --- ## List Invitations > List all pending invitations URL: https://docs.storno.ro/api-reference/invitations/list # List Invitations Retrieve all pending invitations for the organization. --- ## Get Invitations ```http GET /api/v1/invitations ``` Get all pending (not yet accepted or expired) invitations. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response Returns an array of pending invitation objects. ```json [ { "uuid": "770e8400-e29b-41d4-a716-446655440002", "email": "newuser@example.com", "role": "ACCOUNTANT", "createdAt": "2026-02-16T12:00:00Z", "expiresAt": "2026-02-23T12:00:00Z" }, { "uuid": "880e8400-e29b-41d4-a716-446655440003", "email": "another@example.com", "role": "EMPLOYEE", "createdAt": "2026-02-15T10:00:00Z", "expiresAt": "2026-02-22T10:00:00Z" } ] ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | uuid | string | Invitation UUID | | email | string | Invited user email | | role | string | Role assigned to invitation | | createdAt | string | ISO 8601 creation timestamp | | expiresAt | string | ISO 8601 expiry timestamp | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - insufficient permissions | ### Notes - Only returns pending invitations - Accepted invitations are not shown - Expired invitations may still appear until cleaned up - Only organization admins and owners can view invitations --- ## Resend Invitation > Resend invitation email URL: https://docs.storno.ro/api-reference/invitations/resend # Resend Invitation Resend the invitation email for a pending invitation. --- ## Resend Invitation ```http POST /api/v1/invitations/{uuid}/resend ``` Resend the invitation email. This does not extend the expiration date. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | uuid | string | Invitation UUID | ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response Returns `204 No Content` on successful email send. ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - insufficient permissions | | 404 | Invitation not found or already accepted | | 410 | Gone - invitation has expired | ### Notes - Email is sent to the original invitation email address - Expiration date is not extended - If invitation has expired, cancel and create a new invitation instead - Only organization admins and owners can resend invitations - Rate limiting may apply to prevent email abuse --- ## Cancel invoice > Cancel an issued invoice URL: https://docs.storno.ro/api-reference/invoices/cancel # Cancel invoice Cancels an issued invoice by changing its status to `cancelled`. Cancelled invoices remain in the system for record-keeping but are marked as void. A cancellation reason is required. ``` POST /api/v1/invoices/{uuid}/cancel ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | ## Request body | Name | Type | Required | Description | |------|------|----------|-------------| | `reason` | string | Yes | Reason for cancellation (minimum 10 characters) | Cancelled invoices cannot be edited or reissued. To reverse a cancelled invoice, you must create a new invoice. Use the [restore endpoint](/api-reference/invoices/restore) only for accidental cancellations. ## Request ```bash curl -X POST https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/cancel \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{ "reason": "Client requested cancellation due to incorrect billing information" }' ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/cancel', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000', 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: 'Client requested cancellation due to incorrect billing information' }) }); const invoice = await response.json(); ``` ## Response Returns the updated invoice object with status `cancelled`. ```json { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "number": "FAC-2024-001", "status": "cancelled", "direction": "outgoing", "currency": "RON", "issueDate": "2024-02-15", "dueDate": "2024-03-15", "subtotal": 1000.00, "vatTotal": 190.00, "total": 1190.00, "amountPaid": 0.00, "balance": 0.00, "cancellationReason": "Client requested cancellation due to incorrect billing information", "cancelledAt": "2024-02-16T14:30:00Z", "cancelledBy": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "John Doe", "email": "john@yourcompany.ro" }, "events": [ { "id": "6f7a8b9c-0d1e-2f3a-4b5c-6d7e8f9a0b1c", "type": "status_change", "status": "cancelled", "timestamp": "2024-02-16T14:30:00Z", "details": "Invoice cancelled", "metadata": { "reason": "Client requested cancellation due to incorrect billing information" } } ], "updatedAt": "2024-02-16T14:30:00Z" } ``` ## What happens when you cancel an invoice 1. **Status change** - Invoice status changes to `cancelled` 2. **Balance zeroed** - Any remaining balance is set to zero 3. **Event logged** - Cancellation event recorded with reason and user 4. **Audit trail** - Cancellation cannot be undone (except via restore for accidental cancellations) 5. **Provider notification** - If the invoice was submitted to the e-invoice provider, a cancellation notice may be sent (depending on provider requirements) To properly reverse an invoice for accounting purposes, consider creating a **credit note** instead of cancelling. Credit notes provide a proper audit trail and are the preferred method for invoice corrections. ## When to cancel vs. credit note | Scenario | Action | |----------|--------| | Invoice not yet sent to client | Cancel | | Duplicate invoice created | Cancel | | Invoice created in error | Cancel | | Need to correct amounts/items | Create credit note | | Partial refund required | Create credit note | | Already recorded in accounting | Create credit note | ## Error codes | Code | Description | |------|-------------| | `400` | Validation error - missing or invalid cancellation reason | | `401` | Missing or invalid authentication token | | `403` | No access to the specified company or insufficient permissions | | `404` | Invoice not found | | `409` | Invoice cannot be cancelled in current status (e.g., already cancelled) | | `422` | Business rule violation (e.g., invoice has associated credit notes) | ## Related endpoints - [Restore invoice](/api-reference/invoices/restore) - Restore a cancelled invoice - [Create credit note](/api-reference/credit-notes/create) - Create a credit note to reverse an invoice - [Invoice events](/api-reference/invoices/events) - View cancellation history --- ## Create invoice > Create a new draft invoice URL: https://docs.storno.ro/api-reference/invoices/create # Create invoice Creates a new draft invoice for the specified company. ``` POST /api/v1/invoices ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Request body | Name | Type | Required | Description | |------|------|----------|-------------| | `clientId` | string | No | Client UUID. Either `clientId` or `receiverName` should be provided. | | `receiverName` | string | No | Receiver name (when no client entity exists). Either `clientId` or `receiverName` should be provided. | | `receiverCif` | string | No | Receiver tax ID / CIF (used with `receiverName`) | | `documentSeriesId` | string | No | Invoice series UUID (uses default if not provided) | | `documentType` | string | No | Document type (e.g., `invoice`, `credit_note`) | | `parentDocumentId` | string | No | Parent document UUID (for refunds/credit notes) | | `issueDate` | string | Yes | Invoice issue date (ISO 8601: YYYY-MM-DD) | | `dueDate` | string | No | Payment due date (ISO 8601: YYYY-MM-DD) | | `currency` | string | No | ISO 4217 currency code (default: RON) | | `exchangeRate` | number | No | Exchange rate (default: 1.0) | | `invoiceTypeCode` | string | No | UBL invoice type code (default: 380) | | `notes` | string | No | Public notes visible to client | | `paymentTerms` | string | No | Payment terms description | | `paymentMethod` | string | No | Payment method: `bank_transfer` (default), `cash`, `card`, `cheque`, `other` | | `deliveryLocation` | string | No | Delivery address | | `projectReference` | string | No | Project reference number | | `orderNumber` | string | No | Purchase order number | | `contractNumber` | string | No | Contract reference number | | `issuerName` | string | No | Name of person issuing the invoice | | `issuerId` | string | No | Issuer ID number | | `mentions` | string | No | Additional legal mentions | | `internalNote` | string | No | Internal note (not visible to client) | | `salesAgent` | string | No | Sales agent name | | `deputyName` | string | No | Deputy/representative name | | `deputyIdentityCard` | string | No | Deputy ID card number | | `deputyAuto` | string | No | Deputy vehicle registration | | `language` | string | No | Document language for PDF generation: `ro`, `en`, `de`, `fr` (default: `ro`) | | `tvaLaIncasare` | boolean | No | VAT on collection / TVA la încasare (default: false) | | `platitorTva` | boolean | No | Whether sender is VAT payer (default: false) | | `plataOnline` | boolean | No | Enable online payment via Stripe (default: from company Stripe Connect settings) | | `showClientBalance` | boolean | No | Show client balance on invoice (default: false) | | `clientBalanceExisting` | string | No | Existing client balance amount | | `clientBalanceOverdue` | string | No | Overdue client balance amount | | `collect` | object | No | Create immediate payment on the invoice (see below) | | `autoApplyVatRules` | boolean | No | Auto-apply EU VAT rules: reverse charge (0% VAT) for VIES-valid EU clients, OSS destination country VAT rate for non-VIES EU clients (default: false) | | `vatIncluded` | boolean | No | When used with `autoApplyVatRules`, sets whether unit prices include VAT on all lines. This ensures correct totals after VAT rules change rates (e.g., reverse charge sets VAT to 0%). Without this, use per-line `vatIncluded` instead. | | `idempotencyKey` | string | No | Stable key that lets you safely retry a failed request — see [Idempotency](#idempotency) | | `ublExtensions` | object | No | UBL extension fields for advanced e-Factura compliance (see below) | | `lines` | array | Yes | Array of invoice line items | ### Collect object When provided, creates an immediate payment record on the invoice. | Name | Type | Required | Description | |------|------|----------|-------------| | `value` | number | No | Payment amount (defaults to invoice total) | | `type` | string | No | Payment method: `bank_transfer`, `cash`, `card`, etc. (default: `bank_transfer`) | | `issueDate` | string | No | Payment date (ISO 8601: YYYY-MM-DD) | | `documentNumber` | string | No | Payment reference/document number | | `mentions` | string | No | Payment notes | ### Invoice line object | Name | Type | Required | Description | |------|------|----------|-------------| | `description` | string | Yes | Line item description | | `quantity` | number | Yes | Quantity (must be positive, or non-zero for refunds) | | `unitPrice` | number | Yes | Unit price (must be non-negative) | | `vatRate` | number | No | VAT rate percentage (default: 21.00) | | `vatCategoryCode` | string | No | UBL VAT category code (default: `S`). Usually not needed — auto-determined from `vatRate`: 0% rate auto-corrects to `Z`, and zero-rate codes with rate > 0 auto-correct to `S`. Only set explicitly for special categories like `AE` (reverse charge), `E` (exempt), `K` (intra-community), `G` (export). | | `vatRateId` | string | No | VAT rate UUID (uses default if not provided) | | `unitOfMeasure` | string | No | Unit of measure (e.g., "hours", "pcs", "kg") | | `productId` | string | No | Product UUID (optional reference) | | `discount` | number | No | Fixed discount amount | | `discountPercent` | number | No | Discount percentage | | `vatIncluded` | boolean | No | Whether price includes VAT (default: false) | | `productCode` | string | No | Product code for reference | | `ublExtensions` | object | No | Line-level UBL extensions (see below) | ### e-Factura BT fields These optional fields are used for advanced e-Factura (UBL) compliance: | Name | Type | Description | |------|------|-------------| | `taxPointDate` | string | Tax point date (ISO 8601: YYYY-MM-DD) | | `taxPointDateCode` | string | Tax point date code | | `buyerReference` | string | Buyer reference | | `receivingAdviceReference` | string | Receiving advice reference | | `despatchAdviceReference` | string | Despatch advice reference | | `tenderOrLotReference` | string | Tender or lot reference | | `invoicedObjectIdentifier` | string | Invoiced object identifier | | `buyerAccountingReference` | string | Buyer accounting reference | | `businessProcessType` | string | Business process type | | `payeeName` | string | Payee name (if different from seller) | | `payeeIdentifier` | string | Payee identifier | | `payeeLegalRegistrationIdentifier` | string | Payee legal registration identifier | | `payeeBankAccount` | string | IBAN where payment should be sent — sourced from `PaymentMeans/PayeeFinancialAccount/ID` on synced e-Factura invoices, used by the quick-pay QR flow | | `payeeBankName` | string | Bank name from `PaymentMeans/PayeeFinancialAccount/FinancialInstitutionBranch/Name` | ### UBL extensions (document-level) The `ublExtensions` object supports UBL XML elements that don't have dedicated invoice fields. All sub-fields are optional. Unknown keys are silently stripped. | Name | Type | Description | |------|------|-------------| | `invoicePeriod` | object | Billing period: `startDate` (YYYY-MM-DD), `endDate` (YYYY-MM-DD), `descriptionCode` (e.g., "35") | | `delivery` | object | Delivery info: `actualDeliveryDate` (YYYY-MM-DD), `deliveryAddress` object with `streetName`, `cityName`, `countrySubentity`, `countryCode` | | `allowanceCharges` | array | Document-level allowances/charges (max 20). Each: `chargeIndicator` (bool, false=discount), `amount` (numeric string), `taxCategoryCode` (S/Z/E/AE/K/G/O), `taxRate` (numeric string). Optional: `reasonCode`, `reason`, `baseAmount`, `multiplierFactorNumeric` | | `prepaidAmount` | string | Prepaid amount (numeric string >= 0). Reduces PayableAmount in UBL XML | | `additionalDocumentReferences` | array | Additional references (max 10). Each: `id` (required, max 200), optional `documentTypeCode`, `documentDescription` | Document-level `allowanceCharges` adjust TaxTotal and LegalMonetaryTotal in the generated UBL XML. The stored invoice subtotal/vatTotal/total remain unchanged — the adjustments are computed at XML generation time. ### UBL extensions (line-level) Each line item can include a `ublExtensions` object: | Name | Type | Description | |------|------|-------------| | `invoicePeriod` | object | Line billing period: `startDate` (YYYY-MM-DD), `endDate` (YYYY-MM-DD) | | `allowanceCharges` | array | Line-level allowances/charges (max 10). Each: `chargeIndicator` (bool), `amount` (numeric string). Optional: `reasonCode`, `reason`, `baseAmount`, `multiplierFactorNumeric` | | `additionalItemProperties` | array | Item properties (max 20). Each: `name` (max 50 chars), `value` (max 100 chars) | | `originCountry` | string | Item origin country (ISO 3166-1 alpha-2, e.g., "DE") | ### Idempotency Network failures, timeouts, and 5xx errors can leave a client unsure whether the invoice was actually created. Naive retry logic produces duplicate drafts. Storno offers two protections: #### 1. Explicit idempotency key (recommended) Include `idempotencyKey` in the request body. The same key sent in a future request returns the original invoice instead of creating a new one. **The key must be:** - **Stable per logical operation.** Same Stripe payment intent, same shopping-cart checkout, same external order ID → same key, every retry. - **Unique within your namespace.** Prefix with your service name to avoid collisions: `stripe:pi_3TNpEhHy...`, `paddle:txn_01H...`, `myapp:order_42`. - **Up to 255 characters.** Use the underlying transaction ID and you'll be safe. ```json { "clientId": "...", "currency": "USD", "lines": [...], "idempotencyKey": "stripe:pi_3TNpEhHyDIBD6PSZ1ZnXYN4I" } ``` | Outcome | Status | Behaviour | |---|---|---| | First call | `201 Created` | Invoice created, key recorded | | Retry with same key | `201 Created` | The original invoice is returned (same body, same `id`, same `createdAt`); no new row is inserted. Detect a retry on your side by storing/comparing the returned `id` | | Retry with different key | `201 Created` | A new invoice is created — keys must match across retries | The `idempotency_key` column has a unique index — you cannot accidentally have two invoices with the same key. #### 2. Fuzzy fallback (when no key is provided) If your client doesn't send an `idempotencyKey` and a draft for the same `(company, client, currency, total)` was created in the **last hour**, that existing draft is returned instead of creating a new one. This catches retry storms from clients that don't yet support idempotency keys. The fuzzy fallback only matches DRAFT invoices and only within a 60-minute window. It will not interfere with legitimate "create two similar drafts back-to-back" workflows once an hour has passed, and it never affects already-issued invoices. #### Best practice Always send an `idempotencyKey` — it's the safest, most explicit, and survives the 60-minute fuzzy window. Use the underlying business identifier (Stripe payment intent ID, your order ID) prefixed with your service name. ### Common invoice type codes - `380` - Commercial invoice (default) - `381` - Credit note - `384` - Corrected invoice - `389` - Self-billed invoice - `751` - Invoice information for accounting purposes ## Request ```bash curl -X POST https://api.storno.ro/api/v1/invoices \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{ "clientId": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d", "issueDate": "2024-02-15", "dueDate": "2024-03-15", "currency": "RON", "notes": "Payment terms: 30 days net", "paymentTerms": "Net 30", "idempotencyKey": "myapp:order_42", "lines": [ { "description": "Web Development Services", "quantity": 10, "unitPrice": 100.00, "unitOfMeasure": "hours", "vatIncluded": false } ] }' ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000', 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: 'a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d', issueDate: '2024-02-15', dueDate: '2024-03-15', currency: 'RON', notes: 'Payment terms: 30 days net', paymentTerms: 'Net 30', // Same key on every retry of this logical operation — see Idempotency section idempotencyKey: 'myapp:order_42', lines: [ { description: 'Web Development Services', quantity: 10, unitPrice: 100.00, unitOfMeasure: 'hours', vatIncluded: false } ] }) }); const data = await response.json(); // data.invoice — the created invoice // data.validation — UBL validation results ``` ## Response Returns the created invoice object along with UBL validation results, with status `201 Created`. ```json { "invoice": { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "number": "DRAFT-a1b2c3d4", "status": "draft", "direction": "outgoing", "currency": "RON", "exchangeRate": 1.0, "issueDate": "2024-02-15", "dueDate": "2024-03-15", "subtotal": 1000.00, "vatTotal": 190.00, "total": 1190.00, "amountPaid": 0.00, "balance": 1190.00, "notes": "Payment terms: 30 days net", "paymentTerms": "Net 30", "client": { "id": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d", "name": "Acme Corporation SRL", "cif": "RO98765432" }, "lines": [ { "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "description": "Web Development Services", "quantity": 10.0, "unitPrice": 100.00, "unitOfMeasure": "hours", "vatRate": 19.0, "vatAmount": 190.00, "subtotal": 1000.00, "total": 1190.00 } ], "createdAt": "2024-02-15T08:30:00Z", "updatedAt": "2024-02-15T08:30:00Z" }, "validation": { "valid": true, "errors": [], "warnings": [], "schematronAvailable": true } } ``` ## Error codes | Code | Description | |------|-------------| | `400` | Validation error - missing required fields or invalid data | | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Client or series not found | | `422` | Business validation error (e.g., invalid dates, negative amounts) | --- ## Delete invoice > Delete a draft invoice URL: https://docs.storno.ro/api-reference/invoices/delete # Delete invoice Permanently deletes a draft invoice. Only invoices with status `draft` can be deleted. Issued, submitted, or validated invoices cannot be deleted - they must be cancelled instead. ``` DELETE /api/v1/invoices/{uuid} ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | This action is permanent and cannot be undone. To reverse an issued invoice, use the [cancel endpoint](/api-reference/invoices/cancel) instead. ## Request ```bash curl -X DELETE https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7', { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); // Returns 204 No Content on success if (response.status === 204) { console.log('Invoice deleted successfully'); } ``` ## Response Returns `204 No Content` on successful deletion with an empty response body. ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Invoice not found | | `409` | Cannot delete non-draft invoice. Use cancel endpoint for issued invoices. | ## Related endpoints - [Cancel invoice](/api-reference/invoices/cancel) - Cancel an issued invoice - [Restore invoice](/api-reference/invoices/restore) - Restore a cancelled invoice --- ## Download invoice attachment > Download a file attached to an invoice URL: https://docs.storno.ro/api-reference/invoices/attachments # Download invoice attachment Downloads a file that was attached to an invoice. Attachments are binary files (PDFs, images, documents) that have been uploaded and associated with the invoice. ``` GET /api/v1/invoices/{uuid}/attachments/{attachmentId} ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | | `attachmentId` | string | Yes | Attachment UUID | ## Finding attachment IDs Get attachment IDs from the [invoice details](/api-reference/invoices/get) endpoint: ```javascript const invoice = await fetch('/api/v1/invoices/{uuid}').then(r => r.json()); invoice.attachments.forEach(attachment => { console.log(`${attachment.filename} - ${attachment.id}`); }); ``` ## Request ```bash curl https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/attachments/4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -o contract.pdf ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/attachments/4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); // Download the file const a = document.createElement('a'); a.href = url; a.download = 'contract.pdf'; a.click(); // Or open in new tab window.open(url, '_blank'); ``` ## Response Returns the file as binary data with appropriate content type. ### Response headers | Header | Value | Description | |--------|-------|-------------| | `Content-Type` | {mimeType} | MIME type of the file | | `Content-Disposition` | attachment; filename="{filename}" | Original filename | | `Content-Length` | {size} | File size in bytes | | `X-Upload-Date` | 2024-02-15T08:45:00Z | When file was uploaded | ### Common MIME types | File type | MIME type | |-----------|-----------| | PDF | application/pdf | | JPEG | image/jpeg | | PNG | image/png | | Word | application/vnd.openxmlformats-officedocument.wordprocessingml.document | | Excel | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | | ZIP | application/zip | | Text | text/plain | ## Attachment types Attachments can include: ### Supporting documents - Purchase orders - Contracts - Delivery notes - Receipts - Correspondence ### Visual documentation - Product photos - Delivery photos - Signed documents - Scanned invoices ### Technical files - Specifications - Drawings - CAD files - Technical data sheets ## Upload attachments While this endpoint only handles downloads, you can upload attachments via: ```javascript // Upload attachment const formData = new FormData(); formData.append('file', fileBlob, 'contract.pdf'); formData.append('description', 'Service contract'); await fetch('/api/v1/invoices/{uuid}/attachments', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' }, body: formData }); ``` ## Viewing attachments in invoice details List all attachments for an invoice: ```javascript const invoice = await fetch('/api/v1/invoices/{uuid}').then(r => r.json()); invoice.attachments.forEach(attachment => { console.log({ id: attachment.id, filename: attachment.filename, size: attachment.size, mimeType: attachment.mimeType, uploadedAt: attachment.uploadedAt }); }); ``` Example response: ```json { "attachments": [ { "id": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a", "filename": "contract.pdf", "mimeType": "application/pdf", "size": 245678, "uploadedAt": "2024-02-15T08:45:00Z", "uploadedBy": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "John Doe" }, "description": "Service contract" }, { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "filename": "delivery-photo.jpg", "mimeType": "image/jpeg", "size": 128456, "uploadedAt": "2024-02-15T14:20:00Z", "uploadedBy": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "Jane Smith" }, "description": "Proof of delivery" } ] } ``` ## Security ### Access control - Attachments are scoped to company - Requires valid authentication token - Only accessible to company members ### File scanning - All uploads are scanned for malware - Suspicious files are quarantined - Safe files are stored encrypted ### Storage - Files stored on secure cloud storage - Encrypted at rest and in transit - Automatic backups ## File size limits | Plan | Max file size | Max attachments per invoice | Total storage | |------|--------------|----------------------------|---------------| | Freemium | 5 MB | 3 | 100 MB | | Starter | 10 MB | 5 | 500 MB | | Professional | 25 MB | 10 | 5 GB | | Business | 50 MB | 25 | 50 GB | ## Performance Download speeds depend on file size and location: | File size | Typical download time | |-----------|---------------------| | < 1 MB | < 1 second | | 1-5 MB | 1-3 seconds | | 5-25 MB | 3-10 seconds | | 25-50 MB | 10-20 seconds | Files are served from a CDN for optimal performance. ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Invoice or attachment not found | | `410` | Attachment was deleted | ## Use cases - **Audit trail** - Download supporting documents for review - **Client portal** - Allow clients to download attachments - **Accounting** - Retrieve documents for bookkeeping - **Archive** - Download all attachments for backup - **Compliance** - Access documents for tax audits ## Related endpoints - [Get invoice details](/api-reference/invoices/get) - View all attachments and their metadata --- ## Download invoice PDF > Download or generate the PDF representation of an invoice URL: https://docs.storno.ro/api-reference/invoices/pdf # Download invoice PDF Downloads the PDF representation of an invoice. If the PDF has not been generated yet, it will be created on-demand. ``` GET /api/v1/invoices/{uuid}/pdf ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | ## Request ```bash curl https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/pdf \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -o invoice.pdf ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/pdf', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); // Download the file const a = document.createElement('a'); a.href = url; a.download = 'invoice.pdf'; a.click(); // Or open in new tab window.open(url, '_blank'); ``` ## Response Returns the PDF file with `Content-Type: application/pdf`. ### Response headers | Header | Value | Description | |--------|-------|-------------| | `Content-Type` | application/pdf | PDF content type | | `Content-Disposition` | inline; filename="FAC-2024-001.pdf" | Display inline or download | | `Content-Length` | {size} | File size in bytes | | `X-Generated-At` | 2024-02-15T09:00:00Z | When the PDF was generated | ## PDF features The generated PDF includes: ### Header section - Company logo (if configured) - Supplier details (name, CIF, registration, address) - Invoice number and issue date - QR code for quick scanning (optional) ### Client section - Client/customer details - Billing address - Tax identification numbers ### Line items table - Description, quantity, unit price - Unit of measure - VAT rate and amount - Line totals - Discount information (if applicable) ### Totals section - Subtotal (before VAT) - VAT breakdown by rate - Total amount due - Amount paid (if any) - Balance remaining ### Footer section - Payment terms and due date - Bank account details - Notes and mentions - Legal footer text - Page numbers ### Customization options PDF appearance can be customized in company settings: - Logo and branding colors - Template layout (classic, modern, minimal) - Font and font size - Show/hide optional fields - Custom footer text - Signature image ## When is PDF generated The PDF is generated automatically or on-demand: 1. **Automatic** - When invoice is issued 2. **On-demand** - First time this endpoint is called 3. **Regeneration** - When invoice is updated and reissued ## Performance - **First generation**: 1-3 seconds - **Cached PDF**: < 100ms - **Complex invoices** (50+ lines): 3-5 seconds For large invoices, consider using asynchronous generation: ```javascript // Check if PDF exists const invoice = await fetch('/api/v1/invoices/{uuid}').then(r => r.json()); if (invoice.pdfGenerated) { // Download immediately window.open('/api/v1/invoices/{uuid}/pdf', '_blank'); } else { // Show loading state and poll await generatePdf(); } ``` ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to company | | `404` | Invoice not found | | `422` | Invoice cannot be rendered as PDF (invalid data) | | `500` | PDF generation failed (temporary error) | ## Use cases - Email invoices to clients - Print physical copies - Archive for compliance - Client self-service portal - Attach to payment reminders - Accounting system integration ## Related endpoints - [Download XML](/api-reference/invoices/xml) - Download UBL XML version - [Email invoice](/api-reference/invoices/email) - Email the PDF to client - [Issue invoice](/api-reference/invoices/issue) - Generate invoice files --- ## Download invoice XML > Download the UBL 2.1 XML representation of an invoice URL: https://docs.storno.ro/api-reference/invoices/xml # Download invoice XML Downloads the UBL 2.1 XML file for an issued invoice. The XML is generated automatically when an invoice is issued and is the format required for e-invoice provider submission. ``` GET /api/v1/invoices/{uuid}/xml ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | ## Request ```bash curl https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/xml \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -o invoice.xml ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/xml', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const xmlText = await response.text(); // Save to file or process const blob = new Blob([xmlText], { type: 'application/xml' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'invoice.xml'; a.click(); ``` ## Response Returns the XML file with `Content-Type: application/xml`. ```xml urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1 FAC-2024-001 2024-02-15 2024-03-15 380 RON Your Company SRL Bulevardul Principal 456 Bucharest RO RO12345678 VAT Acme Corporation SRL Strada Exemplu 123 Bucharest RO RO98765432 VAT 190.00 1000.00 190.00 S 19.00 VAT 1000.00 1000.00 1190.00 1190.00 1 10.0 1000.00 Web Development Services S 19.00 VAT 100.00 ``` ### Response headers | Header | Value | Description | |--------|-------|-------------| | `Content-Type` | application/xml | XML content type | | `Content-Disposition` | attachment; filename="FAC-2024-001.xml" | Suggested filename | | `Content-Length` | {size} | File size in bytes | ## UBL 2.1 compliance The generated XML conforms to: - **UBL 2.1** - Universal Business Language version 2.1 - **EN 16931** - European standard for electronic invoicing - **CIUS-RO** - Romanian Core Invoice Usage Specification - **ANAF e-Factura** - Romanian tax authority requirements ## When is XML generated The XML file is automatically generated when you: 1. [Issue an invoice](/api-reference/invoices/issue) 2. Manually trigger XML generation (via issue endpoint even if already issued) ## Use cases - Download for manual submission to e-invoice provider - Archive for compliance and audit purposes - Integration with accounting systems - Validation with external tools - Backup and disaster recovery ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Invoice not found or XML not generated yet | | `410` | XML file was deleted (regenerate by reissuing) | ## Related endpoints - [Issue invoice](/api-reference/invoices/issue) - Generate XML file - [Download PDF](/api-reference/invoices/pdf) - Download PDF version - [Submit to e-invoice provider](/api-reference/invoices/submit) - Submit the XML to e-invoice provider - [Validate invoice](/api-reference/invoices/validate) - Validate XML compliance --- ## Email invoice > Send an invoice via email with PDF and XML attachments URL: https://docs.storno.ro/api-reference/invoices/email # Email invoice Sends an invoice to a client via email with optional PDF and XML attachments. The email can be customized with subject, body, and recipients. ``` POST /api/v1/invoices/{uuid}/email ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | ## Request body | Name | Type | Required | Description | |------|------|----------|-------------| | `to` | string | Yes | Recipient email address | | `cc` | string | No | CC email address (comma-separated for multiple) | | `bcc` | string | No | BCC email address (comma-separated for multiple) | | `subject` | string | No | Email subject (uses default template if not provided) | | `body` | string | No | Email body (uses default template if not provided) | | `attachPdf` | boolean | No | Attach PDF invoice (default: true) | | `attachXml` | boolean | No | Attach UBL XML (default: false) | | `language` | string | No | Email template language: `ro`, `en` (default: ro) | Use the [email defaults endpoint](/api-reference/invoices/email-defaults) to get pre-filled subject and body based on your company's email template. ## Request ```bash curl -X POST https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/email \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{ "to": "billing@acme.ro", "cc": "accounting@acme.ro", "subject": "Factura FAC-2024-001", "body": "Buna ziua,\n\nGasiti atasat factura FAC-2024-001 in valoare de 1,190.00 RON.\n\nMultumim!", "attachPdf": true, "attachXml": false }' ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/email', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000', 'Content-Type': 'application/json' }, body: JSON.stringify({ to: 'billing@acme.ro', cc: 'accounting@acme.ro', subject: 'Factura FAC-2024-001', body: 'Buna ziua,\n\nGasiti atasat factura FAC-2024-001 in valoare de 1,190.00 RON.\n\nMultumim!', attachPdf: true, attachXml: false }) }); const result = await response.json(); ``` ## Response Returns a success confirmation with email details. ```json { "success": true, "emailId": "8f9a0b1c-2d3e-4f5a-6b7c-8d9e0f1a2b3c", "to": "billing@acme.ro", "cc": "accounting@acme.ro", "subject": "Factura FAC-2024-001", "attachments": ["FAC-2024-001.pdf"], "sentAt": "2024-02-15T09:05:00Z", "deliveryStatus": "sent" } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `success` | boolean | Whether email was sent successfully | | `emailId` | string | Email tracking UUID | | `to` | string | Recipient email address | | `cc` | string | CC recipients (if any) | | `subject` | string | Email subject that was sent | | `attachments` | array | List of attached file names | | `sentAt` | string | ISO 8601 timestamp | | `deliveryStatus` | string | Email status: `sent`, `queued`, `failed` | ## Email template variables When using default templates, the following variables are automatically replaced: | Variable | Description | Example | |----------|-------------|---------| | `{invoice_number}` | Invoice number | FAC-2024-001 | | `{invoice_total}` | Formatted total amount | 1,190.00 RON | | `{client_name}` | Client name | Acme Corporation SRL | | `{company_name}` | Your company name | Your Company SRL | | `{due_date}` | Payment due date | 15.03.2024 | | `{issue_date}` | Invoice issue date | 15.02.2024 | | `{payment_link}` | Payment link (if enabled) | https://pay.storno.ro/... | ### Default subject template (Romanian) ``` Factura {invoice_number} de la {company_name} ``` ### Default body template (Romanian) ``` Buna ziua, Gasiti atasat factura {invoice_number} in valoare de {invoice_total}. Detalii factura: - Numar: {invoice_number} - Data emiterii: {issue_date} - Scadenta: {due_date} - Total: {invoice_total} Multumim! {company_name} ``` ## Email delivery Emails are sent asynchronously via a queue system: 1. **Immediate** - Email queued immediately (response within 200ms) 2. **Processing** - Email sent within 1-5 seconds 3. **Delivered** - Email delivered to recipient server 4. **Tracking** - Delivery status updated via webhooks You can track delivery status via: - [Email history endpoint](/api-reference/invoices/email-history) - Webhooks (email.delivered, email.bounced events) - Email tracking dashboard ## Email configuration Email sending requires proper configuration in company settings: - **From address** - Verified sender email - **From name** - Display name - **Reply-to** - Reply email address - **SMTP settings** - Custom SMTP (optional) - **Email template** - Default subject and body The sender email must be verified before you can send emails. Unverified emails will result in a `403` error. ## Error codes | Code | Description | |------|-------------| | `400` | Validation error - invalid email address or missing required fields | | `401` | Missing or invalid authentication token | | `403` | Sender email not verified | | `404` | Invoice not found | | `422` | Invoice not issued or PDF/XML not generated | | `429` | Rate limit exceeded (max 100 emails per hour) | | `500` | Email service temporarily unavailable | ## Related endpoints - [Email defaults](/api-reference/invoices/email-defaults) - Get pre-filled email content - [Email history](/api-reference/invoices/email-history) - View sent emails - [Download PDF](/api-reference/invoices/pdf) - Get the PDF file - [Download XML](/api-reference/invoices/xml) - Get the XML file --- ## Export invoices to CSV > Export a filtered list of invoices to CSV format URL: https://docs.storno.ro/api-reference/invoices/export-csv # Export invoices to CSV Exports invoices to a CSV (Comma-Separated Values) file. Supports the same filtering options as the list endpoint for customized exports. ``` GET /api/v1/invoices/export/csv ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Query parameters Accepts the same filter parameters as the [list invoices](/api-reference/invoices/list) endpoint: | Name | Type | Default | Description | |------|------|---------|-------------| | `search` | string | - | Search term for invoice number or client name | | `status` | string | - | Filter by document status | | `direction` | string | - | Filter by direction: `incoming` or `outgoing` | | `from` | string | - | Start date filter (ISO 8601 format: YYYY-MM-DD) | | `to` | string | - | End date filter (ISO 8601 format: YYYY-MM-DD) | | `clientId` | string | - | Filter by client UUID | | `sort` | string | issueDate | Field to sort by | | `order` | string | desc | Sort order: `asc` or `desc` | Large exports (>1000 invoices) may take several seconds. For exports with >5000 invoices, consider using the [ZIP export endpoint](/api-reference/invoices/export-zip) instead. ## Request ```bash curl "https://api.storno.ro/api/v1/invoices/export/csv?from=2024-01-01&to=2024-12-31&status=issued" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -o invoices-2024.csv ``` ```javascript const params = new URLSearchParams({ from: '2024-01-01', to: '2024-12-31', status: 'issued' }); const response = await fetch(`https://api.storno.ro/api/v1/invoices/export/csv?${params}`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'invoices-2024.csv'; a.click(); ``` ## Response Returns a CSV file with `Content-Type: text/csv; charset=utf-8`. ### Response headers | Header | Value | Description | |--------|-------|-------------| | `Content-Type` | text/csv; charset=utf-8 | CSV with UTF-8 encoding | | `Content-Disposition` | attachment; filename="invoices-2024-01-01.csv" | Suggested filename | | `X-Total-Records` | 156 | Number of invoices exported | ### CSV structure ```csv Invoice Number,Client Name,Client CIF,Issue Date,Due Date,Status,Currency,Subtotal,VAT Total,Total,Amount Paid,Balance,Direction,Series,Notes FAC-2024-001,Acme Corporation SRL,RO98765432,2024-02-15,2024-03-15,issued,RON,1000.00,190.00,1190.00,500.00,690.00,outgoing,FAC,Payment terms: 30 days net FAC-2024-002,Beta Industries SRL,RO11223344,2024-02-16,2024-03-16,validated,RON,2500.00,475.00,2975.00,2975.00,0.00,outgoing,FAC, FAC-2024-003,Gamma Trading SRL,RO55667788,2024-02-17,2024-03-17,sent_to_provider,EUR,1200.00,228.00,1428.00,0.00,1428.00,outgoing,FAC,International client ``` ## CSV columns | Column | Description | |--------|-------------| | Invoice Number | Full invoice number | | Client Name | Client company name | | Client CIF | Client tax ID (CIF/VAT number) | | Issue Date | Invoice issue date (YYYY-MM-DD) | | Due Date | Payment due date (YYYY-MM-DD) | | Status | Current invoice status | | Currency | ISO currency code | | Subtotal | Total before VAT | | VAT Total | Total VAT amount | | Total | Final total including VAT | | Amount Paid | Total payments received | | Balance | Remaining balance due | | Direction | incoming/outgoing | | Series | Invoice series name | | Notes | Invoice notes/description | ### Additional columns | Column | Description | |--------|-------------| | Project Reference | Project reference number | | Order Number | Purchase order number | | Contract Number | Contract reference | | Sales Agent | Sales agent name | | Payment Terms | Payment terms description | | ANAF Submission ID | E-invoice provider submission identifier | | Internal Note | Internal notes | | Created At | When invoice was created | | Updated At | Last modification date | ## CSV formatting - **Encoding** - UTF-8 with BOM for Excel compatibility - **Delimiter** - Comma (,) - **Quote character** - Double quote (") - **Line ending** - CRLF (\r\n) for Windows compatibility - **Numbers** - Decimal point (.), no thousands separator - **Dates** - ISO 8601 format (YYYY-MM-DD) - **Boolean** - true/false (lowercase) ### Excel compatibility The CSV is formatted for optimal Excel compatibility: - UTF-8 BOM for automatic encoding detection - Proper quoting of fields containing commas - Date format recognized by Excel - Numbers formatted as numbers (not text) ## Use cases - **Accounting import** - Import into accounting software - **Reporting** - Create custom reports in Excel/Google Sheets - **Data analysis** - Analyze invoice patterns and trends - **Backup** - Archive invoice data - **Audits** - Provide invoice data to auditors - **Integration** - Feed data to other systems ## Performance | Invoice count | Typical response time | |--------------|----------------------| | < 100 | < 1 second | | 100-500 | 1-3 seconds | | 500-1000 | 3-5 seconds | | 1000-5000 | 5-15 seconds | | > 5000 | Use ZIP export | ## Limitations - **Maximum records** - 10,000 invoices per export - **Timeout** - 30 second timeout - **Rate limiting** - 10 exports per hour For larger exports, use the [ZIP export endpoint](/api-reference/invoices/export-zip). ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `413` | Too many invoices to export (use ZIP export) | | `422` | Invalid filter parameters | | `504` | Export timeout (reduce date range or use filters) | ## Related endpoints - [Export to ZIP](/api-reference/invoices/export-zip) - Export with PDF/XML files - [List invoices](/api-reference/invoices/list) - Preview data before export - [Get invoice details](/api-reference/invoices/get) - Get full invoice data --- ## Export invoices to ZIP > Export invoices with PDF and XML files to a ZIP archive URL: https://docs.storno.ro/api-reference/invoices/export-zip # Export invoices to ZIP Creates a ZIP archive containing invoice data, PDFs, and XML files. This endpoint processes the export asynchronously and returns a download URL when ready. ``` POST /api/v1/invoices/export/zip ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Request body | Name | Type | Required | Description | |------|------|----------|-------------| | `invoiceIds` | array | Yes | Array of invoice UUIDs (max 100) | | `includePdf` | boolean | No | Include PDF files (default: true) | | `includeXml` | boolean | No | Include XML files (default: true) | | `includeCsv` | boolean | No | Include CSV summary (default: true) | | `folderStructure` | string | No | Folder organization: `flat`, `by-client`, `by-month`, `by-series` (default: flat) | The export is processed asynchronously - you'll receive a download URL when the archive is ready. ## Request ```bash curl -X POST https://api.storno.ro/api/v1/invoices/export/zip \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{ "invoiceIds": [ "7c9e6679-7425-40de-944b-e07fc1f90ae7", "8d9e7679-8425-40de-944b-e07fc1f90ae8", "9e9e8679-9425-40de-944b-e07fc1f90ae9" ], "includePdf": true, "includeXml": true, "includeCsv": true, "folderStructure": "by-client" }' ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/export/zip', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000', 'Content-Type': 'application/json' }, body: JSON.stringify({ invoiceIds: [ '7c9e6679-7425-40de-944b-e07fc1f90ae7', '8d9e7679-8425-40de-944b-e07fc1f90ae8', '9e9e8679-9425-40de-944b-e07fc1f90ae9' ], includePdf: true, includeXml: true, includeCsv: true, folderStructure: 'by-client' }) }); const result = await response.json(); // Poll for completion or wait for webhook if (result.status === 'completed') { window.open(result.downloadUrl, '_blank'); } else { // Poll status endpoint await pollExportStatus(result.exportId); } ``` ## Response Returns export job details. The export is processed asynchronously. ### Immediate response (export queued) ```json { "exportId": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d", "status": "processing", "totalInvoices": 3, "progress": 0, "estimatedCompletionTime": "2024-02-16T15:05:00Z", "createdAt": "2024-02-16T15:00:00Z", "statusUrl": "/api/v1/exports/a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d" } ``` ### Completed response (for small exports) ```json { "exportId": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d", "status": "completed", "filename": "invoices-2024-02-16.zip", "downloadUrl": "https://cdn.storno.ro/exports/a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d/invoices-2024-02-16.zip", "expiresAt": "2024-02-23T15:00:00Z", "fileSize": 2457600, "totalInvoices": 3, "filesIncluded": { "pdf": 3, "xml": 3, "csv": 1 }, "createdAt": "2024-02-16T15:00:00Z", "completedAt": "2024-02-16T15:00:15Z" } ``` ## Polling for completion Poll the status URL until the export is complete: ```javascript async function pollExportStatus(exportId) { const maxAttempts = 60; const pollInterval = 5000; // 5 seconds for (let i = 0; i < maxAttempts; i++) { const response = await fetch(`/api/v1/exports/${exportId}`, { headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const status = await response.json(); if (status.status === 'completed') { return status.downloadUrl; } else if (status.status === 'failed') { throw new Error(status.error); } // Update progress bar updateProgress(status.progress); await new Promise(resolve => setTimeout(resolve, pollInterval)); } throw new Error('Export timeout'); } ``` ## Webhook notification (recommended) Instead of polling, configure a webhook to be notified when the export is ready: ```json { "event": "export.completed", "exportId": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d", "downloadUrl": "https://cdn.storno.ro/exports/.../invoices.zip", "expiresAt": "2024-02-23T15:00:00Z" } ``` ## ZIP structure The ZIP archive organization depends on the `folderStructure` parameter: ### Flat structure (default) ``` invoices-2024-02-16.zip ├── invoices.csv ├── FAC-2024-001.pdf ├── FAC-2024-001.xml ├── FAC-2024-002.pdf ├── FAC-2024-002.xml └── FAC-2024-003.pdf ``` ### By client structure ``` invoices-2024-02-16.zip ├── invoices.csv ├── Acme Corporation SRL/ │ ├── FAC-2024-001.pdf │ └── FAC-2024-001.xml ├── Beta Industries SRL/ │ ├── FAC-2024-002.pdf │ └── FAC-2024-002.xml └── Gamma Trading SRL/ ├── FAC-2024-003.pdf └── FAC-2024-003.xml ``` ### By month structure ``` invoices-2024-02-16.zip ├── invoices.csv ├── 2024-01/ │ ├── FAC-2024-001.pdf │ └── FAC-2024-001.xml ├── 2024-02/ │ ├── FAC-2024-002.pdf │ ├── FAC-2024-002.xml │ ├── FAC-2024-003.pdf │ └── FAC-2024-003.xml ``` ### By series structure ``` invoices-2024-02-16.zip ├── invoices.csv ├── FAC/ │ ├── FAC-2024-001.pdf │ ├── FAC-2024-001.xml │ ├── FAC-2024-002.pdf │ └── FAC-2024-002.xml └── PRO/ ├── PRO-2024-001.pdf └── PRO-2024-001.xml ``` ## Export status values | Status | Description | |--------|-------------| | `queued` | Export job is queued | | `processing` | Export is being created | | `completed` | Export is ready for download | | `failed` | Export failed (see error field) | | `expired` | Download link has expired | ## Download expiration - ZIP files expire after **7 days** - Download URLs are valid for **7 days** - After expiration, create a new export ## Performance | Invoice count | Typical processing time | |--------------|------------------------| | < 10 | Immediate (< 1 second) | | 10-50 | 5-15 seconds | | 50-100 | 15-30 seconds | ## Limitations - **Maximum invoices** - 100 invoices per export - **Rate limiting** - 5 exports per hour per company - **Storage duration** - 7 days - **Concurrent exports** - 2 active exports per company For more than 100 invoices, create multiple exports or contact support for bulk export options. ## Error codes | Code | Description | |------|-------------| | `400` | Validation error - missing or invalid parameters | | `401` | Missing or invalid authentication token | | `403` | No access to company | | `404` | One or more invoices not found | | `413` | Too many invoices (max 100) | | `422` | Some invoices have no PDF/XML generated | | `429` | Rate limit exceeded | | `500` | Export service temporarily unavailable | ## Error handling ```javascript try { const response = await fetch('/api/v1/invoices/export/zip', { method: 'POST', body: JSON.stringify({ invoiceIds: [...] }) }); if (!response.ok) { const error = await response.json(); if (response.status === 413) { // Split into multiple exports createMultipleExports(invoiceIds); } else { throw new Error(error.message); } } const result = await response.json(); await pollExportStatus(result.exportId); } catch (error) { console.error('Export failed:', error); } ``` ## Related endpoints - [Export to CSV](/api-reference/invoices/export-csv) - Simple CSV export - [Download PDF](/api-reference/invoices/pdf) - Download single PDF - [Download XML](/api-reference/invoices/xml) - Download single XML - [List invoices](/api-reference/invoices/list) - Get invoice IDs for export --- ## Export invoices, receipts and payments as SAGA XML > Single-file SAGA XML exports for outgoing invoices, receipts (incasari) and supplier payments (plati) URL: https://docs.storno.ro/api-reference/invoices/export-saga-xml # Export invoices, receipts and payments as SAGA XML Three GET endpoints that return one SAGA XML file each — handy when SAGA only needs that single import file (no ZIP, no master data). For a full SAGA bundle (clients, suppliers, products, invoices, receipts, payments) use [`POST /accounting-export/zip`](/api-reference/accounting-export/zip). ## Endpoints ``` GET /api/v1/invoices/export/saga-xml GET /api/v1/invoices/export/receipts-saga-xml GET /api/v1/invoices/export/payments-saga-xml ``` | Endpoint | Returns | Root element | |----------|---------|--------------| | `/invoices/export/saga-xml` | Outgoing invoices for the active company. Accepts the same query filters as the invoice list. | `` | | `/invoices/export/receipts-saga-xml` | Receipts (incasari) on **outgoing** invoices. | `` | | `/invoices/export/payments-saga-xml` | Payments (plati) on **incoming** invoices. | `` | ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Query parameters (receipts and payments) Both `*-saga-xml` payment endpoints accept per-export chart-of-accounts overrides. When omitted, the company’s stored SAGA settings are used (see [accounting export settings](/api-reference/accounting-export/settings)). | Name | Default | Description | |------|---------|-------------| | `currency` | — | Optional ISO 4217 filter (`RON`, `USD`, `EUR`, …). When set, only payments in that currency are exported, the file name is suffixed with `_`, and the per-currency overrides from `saga.currencyAccounts.` are applied on top of the RON defaults. When omitted, all payments are bundled in a single file with the RON account map (legacy behaviour — use the ZIP endpoint for clean per-currency splitting). | | `accountCash` | stored setting / `5311` | Cont used for `cash` (TipDocument `Chitanta`). | | `accountBank` | stored setting / `5121` | Cont used for `bank_transfer` (TipDocument `OP`). | | `accountCard` | stored setting / `5125.2` | Cont used for `card` (TipDocument `Card`). SAGA requires a leaf analytic — `5125` alone is not postable. | ## Example ```bash curl -G 'https://api.storno.ro/api/v1/invoices/export/receipts-saga-xml' \ -H 'Authorization: Bearer ' \ -H 'X-Company: ' \ --data-urlencode 'accountCard=5125.2' \ -o incasari.xml ``` ## Response ```http Content-Type: application/xml; charset=UTF-8 Content-Disposition: attachment; filename="I_RO_multiple__.xml" ``` Body is the SAGA XML file. ## CodFiscal handling For card receipts (`TipDocument=Card`), `` is emitted empty — SAGA reconciles card flows through the merchant aggregator account. For cash and bank transfers, `` is emitted only when the partner has a Romanian CIF/CUI (digits, optional `RO` prefix). Foreign VAT codes (e.g. `DE...`, `BG...`) and internal client IDs are dropped to keep SAGA from rejecting the import line. --- ## Get email defaults > Retrieve pre-filled email content from company template URL: https://docs.storno.ro/api-reference/invoices/email-defaults # Get email defaults Retrieves pre-filled email content for an invoice based on your company's email template settings. Use this endpoint to populate the email form before sending. ``` GET /api/v1/invoices/{uuid}/email-defaults ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | ## Query parameters | Name | Type | Default | Description | |------|------|---------|-------------| | `language` | string | ro | Email language: `ro` or `en` | ## Request ```bash curl https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/email-defaults \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/email-defaults', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const emailDefaults = await response.json(); // Use to populate email form document.getElementById('email-to').value = emailDefaults.to; document.getElementById('email-subject').value = emailDefaults.subject; document.getElementById('email-body').value = emailDefaults.body; ``` ## Response Returns pre-filled email data with template variables replaced. ```json { "to": "billing@acme.ro", "cc": "accounting@acme.ro", "bcc": null, "subject": "Factura FAC-2024-001 de la Your Company SRL", "body": "Buna ziua,\n\nGasiti atasat factura FAC-2024-001 in valoare de 1,190.00 RON.\n\nDetalii factura:\n- Numar: FAC-2024-001\n- Data emiterii: 15.02.2024\n- Scadenta: 15.03.2024\n- Total: 1,190.00 RON\n\nPuteti efectua plata aici: https://pay.storno.ro/inv_abc123\n\nMultumim!\n\nYour Company SRL\ncontact@yourcompany.ro\n+40 123 456 789", "attachPdf": true, "attachXml": false, "from": { "email": "contact@yourcompany.ro", "name": "Your Company SRL" }, "replyTo": "accounting@yourcompany.ro" } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `to` | string | Client billing email (from client record) | | `cc` | string\|null | CC address (from company settings) | | `bcc` | string\|null | BCC address (from company settings) | | `subject` | string | Pre-filled subject with variables replaced | | `body` | string | Pre-filled body with variables replaced | | `attachPdf` | boolean | Default PDF attachment setting | | `attachXml` | boolean | Default XML attachment setting | | `from` | object | Sender email and name | | `replyTo` | string | Reply-to email address | ## Template variables The following variables are automatically replaced in subject and body: | Variable | Example value | Description | |----------|---------------|-------------| | `{invoice_number}` | FAC-2024-001 | Invoice number | | `{invoice_total}` | 1,190.00 RON | Formatted total with currency | | `{invoice_subtotal}` | 1,000.00 RON | Subtotal before VAT | | `{invoice_vat}` | 190.00 RON | Total VAT amount | | `{client_name}` | Acme Corporation SRL | Client company name | | `{client_contact}` | John Smith | Client contact person | | `{company_name}` | Your Company SRL | Your company name | | `{company_cif}` | RO12345678 | Your company CIF | | `{company_email}` | contact@yourcompany.ro | Company email | | `{company_phone}` | +40 123 456 789 | Company phone | | `{due_date}` | 15.03.2024 | Payment due date | | `{issue_date}` | 15.02.2024 | Invoice issue date | | `{payment_link}` | https://pay.storno.ro/inv_abc123 | Payment link (if enabled) | | `{days_until_due}` | 28 | Days until payment is due | | `{series_name}` | Facturi 2024 | Invoice series name | ## Customizing email templates Email templates can be customized in company settings: ### Subject template ``` Factura {invoice_number} de la {company_name} - Scadenta: {due_date} ``` ### Body template ``` Stimate {client_name}, Va transmitem factura {invoice_number} in valoare totala de {invoice_total}. Detalii: - Data emiterii: {issue_date} - Termen de plata: {due_date} ({days_until_due} zile) - Subtotal: {invoice_subtotal} - TVA: {invoice_vat} - TOTAL: {invoice_total} Puteti efectua plata online: {payment_link} Cu stima, {company_name} ``` ### Conditional sections Templates support basic conditional logic: ``` {if payment_link} Plata online: {payment_link} {endif} {if overdue} ATENTIE: Aceasta factura este restanta! {endif} ``` ## Client email resolution The `to` field is automatically populated from: 1. Client billing email (if set) 2. Client primary email 3. Client contact person email 4. Empty (user must fill manually) ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Invoice not found | | `422` | Invalid language parameter | ## Use cases - **Pre-fill email form** - Load defaults when user clicks "Send Email" - **Bulk email preview** - Show what will be sent before batch sending - **Template testing** - Preview how templates look with real data - **API integration** - Get consistent email content in external systems ## Related endpoints - [Send email](/api-reference/invoices/email) - Send the email with these defaults - [Email history](/api-reference/invoices/email-history) - View previously sent emails - [Get invoice details](/api-reference/invoices/get) - Get invoice data for custom templates --- ## Get email history > Retrieve the history of emails sent for an invoice URL: https://docs.storno.ro/api-reference/invoices/email-history # Get email history Retrieves the complete history of emails sent for a specific invoice, including delivery status, timestamps, and recipients. ``` GET /api/v1/invoices/{uuid}/emails ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | ## Request ```bash curl https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/emails \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/emails', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const emailHistory = await response.json(); // Display email history emailHistory.forEach(email => { console.log(`Sent to ${email.to} on ${email.sentAt}: ${email.deliveryStatus}`); }); ``` ## Response Returns an array of email records, ordered by sent date (newest first). ```json [ { "id": "9a0b1c2d-3e4f-5a6b-7c8d-9e0f1a2b3c4d", "to": "billing@acme.ro", "cc": "accounting@acme.ro", "bcc": null, "subject": "Factura FAC-2024-001 de la Your Company SRL", "attachments": ["FAC-2024-001.pdf"], "sentAt": "2024-02-15T09:05:00Z", "deliveryStatus": "delivered", "openedAt": "2024-02-15T10:30:00Z", "clickedAt": "2024-02-15T10:31:00Z", "bouncedAt": null, "bounceReason": null, "sentBy": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "John Doe", "email": "john@yourcompany.ro" }, "metadata": { "userAgent": "Mozilla/5.0...", "ipAddress": "192.168.1.1" } }, { "id": "8f9a0b1c-2d3e-4f5a-6b7c-8d9e0f1a2b3c", "to": "billing@acme.ro", "cc": null, "bcc": null, "subject": "Reminder: Factura FAC-2024-001", "attachments": ["FAC-2024-001.pdf"], "sentAt": "2024-02-20T08:00:00Z", "deliveryStatus": "sent", "openedAt": null, "clickedAt": null, "bouncedAt": null, "bounceReason": null, "sentBy": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "System", "email": null }, "metadata": { "automatic": true, "reminderType": "payment_reminder" } }, { "id": "7e8f9a0b-1c2d-3e4f-5a6b-7c8d9e0f1a2b", "to": "old-email@acme.ro", "cc": null, "bcc": null, "subject": "Factura FAC-2024-001", "attachments": ["FAC-2024-001.pdf"], "sentAt": "2024-02-14T15:00:00Z", "deliveryStatus": "bounced", "openedAt": null, "clickedAt": null, "bouncedAt": "2024-02-14T15:01:00Z", "bounceReason": "Mailbox does not exist", "sentBy": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "John Doe", "email": "john@yourcompany.ro" }, "metadata": null } ] ``` ## Email record fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Email record UUID | | `to` | string | Primary recipient email address | | `cc` | string\|null | CC recipients (comma-separated) | | `bcc` | string\|null | BCC recipients (comma-separated) | | `subject` | string | Email subject line | | `attachments` | array | List of attached file names | | `sentAt` | string | When email was sent (ISO 8601) | | `deliveryStatus` | string | Current delivery status | | `openedAt` | string\|null | When email was first opened | | `clickedAt` | string\|null | When a link was first clicked | | `bouncedAt` | string\|null | When email bounced | | `bounceReason` | string\|null | Reason for bounce | | `sentBy` | object | User who sent the email | | `metadata` | object\|null | Additional tracking data | ## Delivery status values | Status | Description | |--------|-------------| | `queued` | Email is queued for sending | | `sent` | Email sent to recipient's mail server | | `delivered` | Email delivered to recipient's inbox | | `opened` | Email was opened by recipient | | `clicked` | Recipient clicked a link in the email | | `bounced` | Email bounced (hard or soft bounce) | | `failed` | Email sending failed | | `spam` | Email marked as spam by recipient | ## Bounce reasons Common bounce reasons include: - `Mailbox does not exist` - Invalid email address - `Mailbox full` - Recipient's mailbox is full - `Domain not found` - Invalid domain - `Rejected by recipient` - Recipient server rejected - `Spam filter` - Blocked by spam filter - `Temporary failure` - Temporary delivery issue ## Email tracking Email tracking provides insights into recipient engagement: ### Open tracking - Enabled by default for all emails - Tracked via invisible pixel - Records first open timestamp - Multiple opens are recorded in metadata ### Click tracking - Tracks links clicked in email body - Rewrites URLs through tracking proxy - Records first click timestamp - Individual link clicks in metadata ### Disable tracking ```javascript // Disable tracking when sending await fetch('/api/v1/invoices/{uuid}/email', { method: 'POST', body: JSON.stringify({ to: 'billing@acme.ro', subject: 'Invoice', body: 'Message', tracking: false // Disable open/click tracking }) }); ``` ## Use cases - **Audit trail** - Verify when invoices were sent to clients - **Delivery confirmation** - Check if client received the invoice - **Engagement tracking** - See if client opened the email - **Resend logic** - Resend if previous attempt bounced - **Client support** - Help clients who claim they didn't receive invoice - **Analytics** - Analyze email performance and delivery rates ## Filter by delivery status (future) While not currently supported, future versions may support filtering: ``` GET /api/v1/invoices/{uuid}/emails?status=bounced GET /api/v1/invoices/{uuid}/emails?from=2024-01-01&to=2024-12-31 ``` ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Invoice not found | ## Related endpoints - [Send email](/api-reference/invoices/email) - Send a new email - [Email defaults](/api-reference/invoices/email-defaults) - Get email template - [Invoice events](/api-reference/invoices/events) - View all invoice events --- ## Get invoice details > Retrieve complete details for a specific invoice URL: https://docs.storno.ro/api-reference/invoices/get # Get invoice details Retrieves the complete details for a specific invoice, including all line items, events, payments, and attachments. ``` GET /api/v1/invoices/{uuid} ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | ## Request ```bash curl https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const invoice = await response.json(); ``` ## Response Returns the complete invoice object with all details. ```json { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "number": "FAC-2024-001", "status": "issued", "direction": "outgoing", "currency": "RON", "exchangeRate": 1.0, "issueDate": "2024-02-15", "dueDate": "2024-03-15", "subtotal": 1000.00, "vatTotal": 190.00, "total": 1190.00, "amountPaid": 500.00, "balance": 690.00, "invoiceTypeCode": "380", "notes": "Payment terms: 30 days net", "paymentTerms": "Net 30", "deliveryLocation": "Bucharest, Romania", "projectReference": "PRJ-2024-001", "orderNumber": "PO-12345", "contractNumber": "CNT-2024-005", "issuerName": "John Doe", "issuerId": "123456789", "mentions": "Late payment penalty applies", "internalNote": "VIP client", "salesAgent": "Jane Smith", "deputyName": "Ion Popescu", "deputyIdentityCard": "AB123456", "deputyAuto": "B-123-ABC", "penaltyEnabled": true, "penaltyPercentPerDay": 0.05, "penaltyGraceDays": 5, "client": { "id": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d", "name": "Acme Corporation SRL", "cif": "RO98765432", "registrationNumber": "J40/1234/2020", "address": "Strada Exemplu 123", "city": "Bucharest", "country": "RO", "email": "billing@acme.ro", "phone": "+40123456789" }, "supplier": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Your Company SRL", "cif": "RO12345678", "registrationNumber": "J40/5678/2019", "address": "Bulevardul Principal 456", "city": "Bucharest", "country": "RO", "email": "contact@yourcompany.ro", "phone": "+40987654321" }, "series": { "id": "f8e7d6c5-b4a3-4c5d-9e8f-7a6b5c4d3e2f", "name": "FAC", "prefix": "FAC-2024-", "nextNumber": 2 }, "lines": [ { "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "description": "Web Development Services", "quantity": 10.0, "unitPrice": 100.00, "unitOfMeasure": "hours", "vatRate": 19.0, "vatAmount": 190.00, "subtotal": 1000.00, "total": 1190.00, "discount": 0.00, "discountPercent": 0.00, "vatIncluded": false, "productCode": "SRV-001", "product": { "id": "9f8e7d6c-5b4a-3c2d-1e0f-9a8b7c6d5e4f", "name": "Web Development", "code": "SRV-001" } } ], "payments": [ { "id": "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e", "amount": 500.00, "paymentDate": "2024-02-20", "paymentMethod": "bank_transfer", "notes": "Partial payment received", "createdAt": "2024-02-20T10:30:00Z" } ], "events": [ { "id": "3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f", "type": "status_change", "status": "issued", "timestamp": "2024-02-15T09:00:00Z", "details": "Invoice issued successfully", "user": "John Doe" } ], "attachments": [ { "id": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a", "filename": "contract.pdf", "mimeType": "application/pdf", "size": 245678, "uploadedAt": "2024-02-15T08:45:00Z" } ], "xmlGenerated": true, "xmlPath": "/storage/invoices/2024/02/7c9e6679-xml.xml", "pdfGenerated": true, "pdfPath": "/storage/invoices/2024/02/7c9e6679-pdf.pdf", "anafSubmissionId": "ANAF-2024-123456", "anafDownloadId": "DL-987654", "anafValidationErrors": null, "createdAt": "2024-02-15T08:30:00Z", "updatedAt": "2024-02-20T10:30:00Z" } ``` ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Invoice not found | --- ## Get invoice events > Retrieve the timeline of status changes and actions for an invoice URL: https://docs.storno.ro/api-reference/invoices/events # Get invoice events Retrieves the complete timeline of events for an invoice, including status changes, submissions, validations, and user actions. Useful for audit trails and understanding invoice history. ``` GET /api/v1/invoices/{uuid}/events ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | ## Request ```bash curl https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/events \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/events', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const events = await response.json(); ``` ## Response Returns an array of event objects, ordered by timestamp (newest first). ```json [ { "id": "9a0b1c2d-3e4f-5a6b-7c8d-9e0f1a2b3c4d", "type": "anaf_validated", "status": "validated", "timestamp": "2024-02-15T10:00:00Z", "details": "Invoice validated by e-invoice provider successfully", "user": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "System", "email": null }, "metadata": { "validationId": "ANAF-VAL-123456", "downloadId": "DL-987654" } }, { "id": "8f9a0b1c-2d3e-4f5a-6b7c-8d9e0f1a2b3c", "type": "anaf_submitted", "status": "sent_to_provider", "timestamp": "2024-02-15T09:30:00Z", "details": "Invoice submitted to e-invoice provider", "user": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "John Doe", "email": "john@yourcompany.ro" }, "metadata": { "submissionId": "ANAF-2024-123456" } }, { "id": "7e8f9a0b-1c2d-3e4f-5a6b-7c8d9e0f1a2b", "type": "status_change", "status": "issued", "timestamp": "2024-02-15T09:00:00Z", "details": "Invoice issued successfully", "user": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "John Doe", "email": "john@yourcompany.ro" }, "metadata": { "xmlGenerated": true, "pdfGenerated": true } }, { "id": "6d7e8f9a-0b1c-2d3e-4f5a-6b7c8d9e0f1a", "type": "payment_received", "status": null, "timestamp": "2024-02-20T14:30:00Z", "details": "Payment of 500.00 RON received", "user": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "Jane Smith", "email": "jane@yourcompany.ro" }, "metadata": { "paymentId": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a", "amount": 500.00, "currency": "RON", "method": "bank_transfer" } }, { "id": "5c6d7e8f-9a0b-1c2d-3e4f-5a6b7c8d9e0f", "type": "email_sent", "status": null, "timestamp": "2024-02-15T09:05:00Z", "details": "Invoice emailed to billing@acme.ro", "user": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "John Doe", "email": "john@yourcompany.ro" }, "metadata": { "to": "billing@acme.ro", "subject": "Invoice FAC-2024-001", "attachments": ["pdf", "xml"] } }, { "id": "4b5c6d7e-8f9a-0b1c-2d3e-4f5a6b7c8d9e", "type": "created", "status": "draft", "timestamp": "2024-02-15T08:30:00Z", "details": "Invoice created", "user": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "John Doe", "email": "john@yourcompany.ro" }, "metadata": null } ] ``` ## Event types | Type | Description | Status change | |------|-------------|---------------| | `created` | Invoice was created | draft | | `updated` | Invoice data was modified | - | | `status_change` | Status changed manually | varies | | `issued` | Invoice was issued | issued | | `anaf_submitted` | Submitted to e-invoice provider | sent_to_provider | | `anaf_validated` | Provider validation passed | validated | | `anaf_rejected` | Provider validation failed | rejected | | `email_sent` | Invoice emailed to client | - | | `payment_received` | Payment recorded | - | | `payment_deleted` | Payment removed | - | | `cancelled` | Invoice cancelled | cancelled | | `restored` | Invoice restored from cancelled | draft | | `pdf_generated` | PDF file generated | - | | `xml_generated` | XML file generated | - | | `viewed` | Invoice viewed by client | - | | `downloaded` | Invoice downloaded | - | ## Event object fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Event UUID | | `type` | string | Event type identifier | | `status` | string\|null | New status (if status changed) | | `timestamp` | string | ISO 8601 timestamp | | `details` | string | Human-readable event description | | `user` | object\|null | User who triggered the event | | `metadata` | object\|null | Additional event-specific data | ## Use cases - **Audit trail** - Track who did what and when - **Debugging** - Understand why an invoice is in a certain state - **Timeline display** - Show invoice history to users - **Compliance** - Maintain records of all invoice actions - **Notifications** - Trigger alerts based on events - **Analytics** - Analyze invoice lifecycle patterns ## Filtering events (future) While not currently supported, future versions may support filtering: ``` GET /api/v1/invoices/{uuid}/events?type=status_change&from=2024-01-01 ``` ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Invoice not found | ## Related endpoints - [Get invoice details](/api-reference/invoices/get) - Get current invoice state - [Issue invoice](/api-reference/invoices/issue) - Creates issue event - [Submit invoice](/api-reference/invoices/submit) - Creates submission event - [Cancel invoice](/api-reference/invoices/cancel) - Creates cancellation event --- ## Issue invoice > Issue a draft invoice and generate XML/PDF URL: https://docs.storno.ro/api-reference/invoices/issue # Issue invoice Issues a draft invoice by validating the data, generating the UBL 2.1 XML file, generating the PDF representation, and optionally scheduling e-invoice provider submission. Changes the invoice status from `draft` to `issued`. ``` POST /api/v1/invoices/{uuid}/issue ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | ## What happens when you issue an invoice 1. **Validation** - The invoice data is validated against business rules and UBL schema 2. **Number assignment** - If the invoice number is not yet assigned, it gets the next number from the series 3. **XML generation** - A UBL 2.1 compliant XML file is generated 4. **PDF generation** - A formatted PDF invoice is generated 5. **Status change** - The invoice status changes from `draft` to `issued` 6. **Event logging** - An issue event is recorded in the invoice timeline Once issued, the invoice cannot be edited or deleted - only cancelled. This endpoint does not automatically submit to the e-invoice provider. Use the [submit endpoint](/api-reference/invoices/submit) to send the invoice to the provider. ## Request ```bash curl -X POST https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/issue \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/issue', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const invoice = await response.json(); ``` ## Response Returns the updated invoice object with status `issued`. ```json { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "number": "FAC-2024-001", "status": "issued", "direction": "outgoing", "currency": "RON", "issueDate": "2024-02-15", "dueDate": "2024-03-15", "subtotal": 1000.00, "vatTotal": 190.00, "total": 1190.00, "amountPaid": 0.00, "balance": 1190.00, "client": { "id": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d", "name": "Acme Corporation SRL", "cif": "RO98765432" }, "xmlGenerated": true, "xmlPath": "/storage/invoices/2024/02/7c9e6679-xml.xml", "pdfGenerated": true, "pdfPath": "/storage/invoices/2024/02/7c9e6679-pdf.pdf", "events": [ { "id": "3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f", "type": "status_change", "status": "issued", "timestamp": "2024-02-15T09:00:00Z", "details": "Invoice issued successfully" } ], "updatedAt": "2024-02-15T09:00:00Z" } ``` ## Error codes | Code | Description | |------|-------------| | `400` | Validation error - invoice data is invalid or incomplete | | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Invoice not found | | `409` | Invoice is already issued or cannot be issued in current status | | `422` | Business validation error (e.g., missing required fields, invalid amounts) | ## Common validation errors - Missing or invalid client information - Missing invoice lines - Negative or zero line quantities - Invalid VAT rates - Invalid currency code - Missing required tax identification numbers - Issue date in the future - Due date before issue date ## Related endpoints - [Validate invoice](/api-reference/invoices/validate) - Validate invoice before issuing - [Submit invoice](/api-reference/invoices/submit) - Submit issued invoice to e-invoice provider - [Download XML](/api-reference/invoices/xml) - Download the generated XML - [Download PDF](/api-reference/invoices/pdf) - Download the generated PDF --- ## List invoices > Retrieve a paginated list of invoices with optional filtering and sorting URL: https://docs.storno.ro/api-reference/invoices/list # List invoices Retrieves a paginated list of invoices for the specified company. Supports filtering by status, date range, client, and search terms. ``` GET /api/v1/invoices ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Query parameters | Name | Type | Default | Description | |------|------|---------|-------------| | `page` | integer | 1 | Page number for pagination | | `limit` | integer | 20 | Number of items per page (max 100) | | `search` | string | - | Search term for invoice number or client name | | `status` | string | - | Filter by document status (see DocumentStatus enum) | | `direction` | string | - | Filter by direction: `incoming` or `outgoing` | | `from` | string | - | Start date filter (ISO 8601 format: YYYY-MM-DD) | | `to` | string | - | End date filter (ISO 8601 format: YYYY-MM-DD) | | `clientId` | string | - | Filter by client UUID | | `sort` | string | issueDate | Field to sort by (issueDate, number, total, dueDate) | | `order` | string | desc | Sort order: `asc` or `desc` | ### DocumentStatus enum values - `draft` - Invoice is being edited - `issued` - Invoice has been issued - `sent_to_provider` - Submitted to e-invoice provider - `validated` - Provider validation successful - `rejected` - Provider validation failed - `cancelled` - Invoice has been cancelled ## Request ```bash curl https://api.storno.ro/api/v1/invoices?page=1&limit=20&status=issued&direction=outgoing \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices?page=1&limit=20&status=issued&direction=outgoing', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const data = await response.json(); ``` ## Response Returns a paginated list of invoices with summary information. ```json { "data": [ { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "number": "FAC-2024-001", "status": "issued", "direction": "outgoing", "currency": "RON", "issueDate": "2024-02-15", "dueDate": "2024-03-15", "subtotal": 1000.00, "vatTotal": 190.00, "total": 1190.00, "clientName": "Acme Corporation SRL", "amountPaid": 500.00, "balance": 690.00, "supplier": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Your Company SRL", "cif": "RO12345678" } } ], "total": 156, "page": 1, "limit": 20, "pages": 8 } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `data` | array | Array of invoice objects | | `total` | integer | Total number of invoices matching the filters | | `page` | integer | Current page number | | `limit` | integer | Items per page | | `pages` | integer | Total number of pages | ### Invoice object fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Invoice UUID | | `number` | string | Invoice number (series-formatted) | | `status` | string | Current invoice status | | `direction` | string | Invoice direction (incoming/outgoing) | | `currency` | string | ISO 4217 currency code | | `issueDate` | string | Date invoice was issued | | `dueDate` | string | Payment due date | | `subtotal` | number | Total before VAT | | `vatTotal` | number | Total VAT amount | | `total` | number | Final total including VAT | | `clientName` | string | Client/customer name | | `amountPaid` | number | Total amount paid | | `balance` | number | Remaining balance due | | `supplier` | object | Supplier information | ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `422` | Invalid query parameters | --- ## Refund an invoice > Two approaches for issuing refunds — credit notes or negative invoices URL: https://docs.storno.ro/api-reference/invoices/refund # Refund an invoice There are two ways to refund an invoice in Storno: 1. **Credit note** — A formal document linked to the original invoice via `parentDocumentId` 2. **Negative invoice** — A standalone invoice with negative quantities, not linked to any parent Both are valid for ANAF e-Factura and produce the same fiscal result. Choose the approach that fits your workflow. ## Option 1: Credit note (linked to parent) Use this when you want a formal reference between the refund and the original invoice. See the [Credit Notes](/api-reference/credit-notes/create) docs for full details. ```bash curl -X POST https://api.storno.ro/api/v1/invoices \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid" \ -H "Content-Type: application/json" \ -d '{ "isCreditNote": true, "parentDocumentId": "original-invoice-uuid", "clientId": "client-uuid", "seriesId": "credit-note-series-uuid", "issueDate": "2026-03-10", "dueDate": "2026-03-10", "currency": "RON", "invoiceTypeCode": "381", "notes": "Credit note for invoice FAC-2026-045", "lines": [ { "description": "Web Development Services (CREDIT)", "quantity": -10, "unitPrice": 100.00, "vatRateId": "vat-rate-uuid" } ] }' ``` ## Option 2: Negative invoice (standalone) Use this when you don't need a formal link to the original invoice. Simply create a regular invoice with negative quantities. ```bash curl -X POST https://api.storno.ro/api/v1/invoices \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid" \ -H "Content-Type: application/json" \ -d '{ "clientId": "client-uuid", "documentSeriesId": "invoice-series-uuid", "issueDate": "2026-03-10", "dueDate": "2026-03-10", "currency": "RON", "invoiceTypeCode": "381", "notes": "Refund for invoice FAC-2026-045", "lines": [ { "description": "Web Development Services (refund)", "quantity": -10, "unitPrice": 100.00, "vatRateId": "vat-rate-uuid" } ] }' ``` This creates a standard invoice with negative totals. No `parentDocumentId` or `isCreditNote` flag is needed. ## When to use which | | Credit note | Negative invoice | |---|---|---| | Linked to original invoice | Yes | No | | Requires `parentDocumentId` | Yes | No | | Requires `isCreditNote: true` | Yes | No | | Valid for ANAF e-Factura | Yes | Yes | | Shows in credit notes list | Yes | No | | Recommended `invoiceTypeCode` | 381 | 381 | | Use case | Formal corrections, audits | Quick refunds, simpler workflow | ## Notes - Both approaches support partial refunds — just adjust the quantities or add only the lines you want to refund - Use `invoiceTypeCode: "381"` (credit note) for both approaches to signal the refund nature in e-Factura - Negative quantities with positive unit prices are recommended over positive quantities with negative prices - Both are submitted to ANAF the same way via the [submit endpoint](/api-reference/invoices/submit) --- ## Restore cancelled invoice > Restore a cancelled invoice back to draft status URL: https://docs.storno.ro/api-reference/invoices/restore # Restore cancelled invoice Restores a cancelled invoice back to `draft` status. This endpoint should only be used for accidental cancellations. Changes the invoice status from `cancelled` back to `draft`, allowing it to be edited and reissued. ``` POST /api/v1/invoices/{uuid}/restore ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | Restoring an invoice should be done carefully. If the invoice was already sent to clients or submitted to ANAF, restoring may cause confusion. Consider creating a new invoice instead. ## Request ```bash curl -X POST https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/restore \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/restore', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const invoice = await response.json(); ``` ## Response Returns the updated invoice object with status `draft`. ```json { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "number": "FAC-2024-001", "status": "draft", "direction": "outgoing", "currency": "RON", "issueDate": "2024-02-15", "dueDate": "2024-03-15", "subtotal": 1000.00, "vatTotal": 190.00, "total": 1190.00, "amountPaid": 0.00, "balance": 1190.00, "cancellationReason": null, "cancelledAt": null, "cancelledBy": null, "restoredAt": "2024-02-16T15:00:00Z", "restoredBy": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "John Doe", "email": "john@yourcompany.ro" }, "events": [ { "id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d", "type": "status_change", "status": "draft", "timestamp": "2024-02-16T15:00:00Z", "details": "Invoice restored from cancelled status" } ], "updatedAt": "2024-02-16T15:00:00Z" } ``` ## What happens when you restore an invoice 1. **Status change** - Invoice status changes from `cancelled` to `draft` 2. **Cancellation data cleared** - Cancellation reason, date, and user are cleared 3. **Balance restored** - Invoice balance is recalculated based on payments 4. **Editable again** - Invoice can now be edited and reissued 5. **Event logged** - Restoration event recorded with timestamp and user After restoration, you can: - Edit invoice details - Modify line items - Delete the invoice - Reissue the invoice ## When to use restore vs. create new | Scenario | Action | |----------|--------| | Accidental cancellation | Restore | | Invoice number must be preserved | Restore | | Invoice not yet sent | Restore (safe) | | Invoice sent to client | Create new invoice | | Invoice submitted to ANAF | Create new invoice | | Need different amounts | Create new invoice | ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to the specified company or insufficient permissions | | `404` | Invoice not found | | `409` | Invoice is not cancelled or cannot be restored | | `422` | Business rule violation (e.g., invoice has payments or was submitted to ANAF) | ## Restrictions An invoice **cannot** be restored if: - It was submitted to ANAF and validated - It has associated credit notes - It has recorded payments (payments must be removed first) - It was cancelled more than 30 days ago (configurable) - The invoice series has been deleted ## Related endpoints - [Cancel invoice](/api-reference/invoices/cancel) - Cancel an invoice - [Update invoice](/api-reference/invoices/update) - Edit the restored invoice - [Delete invoice](/api-reference/invoices/delete) - Delete the restored draft - [Invoice events](/api-reference/invoices/events) - View restoration history --- ## Submit invoice to e-invoice provider > Submit an issued invoice to the e-invoice provider URL: https://docs.storno.ro/api-reference/invoices/submit # Submit invoice to e-invoice provider Submits an issued invoice to the e-invoice provider. The invoice must be issued before it can be submitted. Changes the invoice status to `sent_to_provider`. ``` POST /api/v1/invoices/{uuid}/submit ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | ## Prerequisites Before submitting an invoice to the e-invoice provider, ensure: 1. The invoice has been [issued](/api-reference/invoices/issue) 2. The company has valid provider credentials configured 3. The invoice XML has been generated 4. The company has an active connection to the e-invoice provider After submission, the e-invoice provider will validate the invoice asynchronously. You can check the validation status by polling the [invoice details](/api-reference/invoices/get) endpoint or by listening to webhook events. ## Request ```bash curl -X POST https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/submit \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/submit', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const invoice = await response.json(); ``` ## Response Returns the updated invoice object with status `sent_to_provider` and submission details. ```json { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "number": "FAC-2024-001", "status": "sent_to_provider", "direction": "outgoing", "currency": "RON", "issueDate": "2024-02-15", "dueDate": "2024-03-15", "subtotal": 1000.00, "vatTotal": 190.00, "total": 1190.00, "xmlGenerated": true, "xmlPath": "/storage/invoices/2024/02/7c9e6679-xml.xml", "anafSubmissionId": "ANAF-2024-123456", "anafDownloadId": "DL-987654", "anafValidationErrors": null, "events": [ { "id": "4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a", "type": "status_change", "status": "sent_to_provider", "timestamp": "2024-02-15T09:15:00Z", "details": "Invoice submitted to e-invoice provider successfully", "metadata": { "submissionId": "ANAF-2024-123456", "downloadId": "DL-987654" } } ], "updatedAt": "2024-02-15T09:15:00Z" } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `anafSubmissionId` | string | ANAF submission identifier | | `anafDownloadId` | string | ANAF download identifier for the uploaded file | | `anafValidationErrors` | array\|null | Validation errors from ANAF (populated after validation) | ## Provider validation process After submission, the e-invoice provider validates the invoice in the background: 1. **Submitted** (`sent_to_provider`) - Invoice uploaded to the provider 2. **Validating** - The provider is processing the invoice 3. **Validated** (`validated`) - Invoice passed provider validation 4. **Rejected** (`rejected`) - Invoice failed provider validation You can monitor the validation status through: - Polling the [invoice details endpoint](/api-reference/invoices/get) - Webhooks (if configured) - [Invoice events endpoint](/api-reference/invoices/events) ## Error codes | Code | Description | |------|-------------| | `400` | Missing provider credentials or invalid configuration | | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Invoice not found | | `409` | Invoice is not issued yet or already submitted | | `422` | XML generation failed or invalid XML format | | `503` | E-invoice provider service temporarily unavailable | ## Common submission errors | Error | Cause | Solution | |-------|-------|----------| | No provider token | Company has not connected to the e-invoice provider | Complete provider OAuth flow | | Token expired | Provider OAuth token has expired | Refresh provider token | | XML not generated | Invoice was not properly issued | Re-issue the invoice | | Provider service down | E-invoice provider is unavailable | Retry later | | Invalid XML format | Data validation failed | Check invoice data and re-issue | ## Related endpoints - [Issue invoice](/api-reference/invoices/issue) - Issue invoice before submitting - [Get invoice details](/api-reference/invoices/get) - Check submission status - [Invoice events](/api-reference/invoices/events) - View submission timeline - [Download XML](/api-reference/invoices/xml) - Download submitted XML --- ## Update invoice > Update an existing editable invoice URL: https://docs.storno.ro/api-reference/invoices/update # Update invoice Updates an existing invoice. Invoices can be updated as long as they are editable — this includes `draft` invoices, `rejected` invoices (so you can fix and resubmit), and issued invoices that have not yet been submitted to ANAF. Invoices with status `cancelled` or `sent_to_provider`, or that have already been uploaded to ANAF, cannot be updated. ``` PUT /api/v1/invoices/{uuid} ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | ## Request body All fields are optional — only include the fields you want to update. | Name | Type | Description | |------|------|-------------| | `clientId` | string | Client UUID | | `receiverName` | string | Receiver name (when no client entity selected) | | `receiverCif` | string | Receiver tax ID / CIF (used with `receiverName`) | | `documentSeriesId` | string | Invoice series UUID (only changeable on draft invoices) | | `documentType` | string | Document type (e.g., `invoice`, `credit_note`) | | `issueDate` | string | Invoice issue date (ISO 8601: YYYY-MM-DD) | | `dueDate` | string | Payment due date (ISO 8601: YYYY-MM-DD) | | `currency` | string | ISO 4217 currency code | | `exchangeRate` | number | Exchange rate | | `invoiceTypeCode` | string | UBL invoice type code | | `notes` | string | Public notes visible to client | | `paymentTerms` | string | Payment terms description | | `paymentMethod` | string | Payment method: `bank_transfer`, `cash`, `card`, `cheque`, `other` | | `deliveryLocation` | string | Delivery address | | `projectReference` | string | Project reference number | | `orderNumber` | string | Purchase order number | | `contractNumber` | string | Contract reference number | | `issuerName` | string | Name of person issuing the invoice | | `issuerId` | string | Issuer ID number | | `mentions` | string | Additional legal mentions | | `internalNote` | string | Internal note (not visible to client) | | `salesAgent` | string | Sales agent name | | `deputyName` | string | Deputy/representative name | | `deputyIdentityCard` | string | Deputy ID card number | | `deputyAuto` | string | Deputy vehicle registration | | `language` | string | Document language for PDF generation: `ro`, `en`, `de`, `fr` | | `tvaLaIncasare` | boolean | VAT on collection (TVA la încasare) | | `platitorTva` | boolean | Whether sender is VAT payer | | `plataOnline` | boolean | Enable online payment via Stripe | | `showClientBalance` | boolean | Show client balance on invoice | | `clientBalanceExisting` | string | Existing client balance amount | | `clientBalanceOverdue` | string | Overdue client balance amount | | `autoApplyVatRules` | boolean | Auto-apply EU VAT rules: reverse charge (0% VAT) for VIES-valid EU clients, OSS destination country VAT rate for non-VIES EU clients (default: false) | | `vatIncluded` | boolean | When used with `autoApplyVatRules`, sets whether unit prices include VAT on all lines. This ensures correct totals after VAT rules change rates (e.g., reverse charge sets VAT to 0%). Without this, use per-line `vatIncluded` instead. | | `ublExtensions` | object | UBL extension fields for advanced e-Factura compliance (see below). Pass `null` to clear. | | `lines` | array | Array of invoice line items (replaces all lines) | ### Invoice line object | Name | Type | Required | Description | |------|------|----------|-------------| | `description` | string | Yes | Line item description | | `quantity` | number | Yes | Quantity | | `unitPrice` | number | Yes | Unit price | | `vatRate` | number | No | VAT rate percentage (default: 21.00) | | `vatCategoryCode` | string | No | UBL VAT category code (default: `S`). Usually not needed — auto-determined from `vatRate`: 0% rate auto-corrects to `Z`, and zero-rate codes with rate > 0 auto-correct to `S`. Only set explicitly for special categories like `AE` (reverse charge), `E` (exempt), `K` (intra-community), `G` (export). | | `vatRateId` | string | No | VAT rate UUID | | `unitOfMeasure` | string | No | Unit of measure (e.g., "hours", "pcs", "kg") | | `productId` | string | No | Product UUID (optional reference) | | `discount` | number | No | Fixed discount amount | | `discountPercent` | number | No | Discount percentage | | `vatIncluded` | boolean | No | Whether price includes VAT (default: false) | | `productCode` | string | No | Product code for reference | | `ublExtensions` | object | No | Line-level UBL extensions (see below) | ### e-Factura BT fields These optional fields are used for advanced e-Factura (UBL) compliance: | Name | Type | Description | |------|------|-------------| | `taxPointDate` | string | Tax point date (ISO 8601: YYYY-MM-DD) | | `taxPointDateCode` | string | Tax point date code | | `buyerReference` | string | Buyer reference | | `receivingAdviceReference` | string | Receiving advice reference | | `despatchAdviceReference` | string | Despatch advice reference | | `tenderOrLotReference` | string | Tender or lot reference | | `invoicedObjectIdentifier` | string | Invoiced object identifier | | `buyerAccountingReference` | string | Buyer accounting reference | | `businessProcessType` | string | Business process type | | `payeeName` | string | Payee name (if different from seller) | | `payeeIdentifier` | string | Payee identifier | | `payeeLegalRegistrationIdentifier` | string | Payee legal registration identifier | | `payeeBankAccount` | string | IBAN where payment should be sent — used by the quick-pay QR flow | | `payeeBankName` | string | Bank name corresponding to `payeeBankAccount` | ### UBL extensions (document-level) The `ublExtensions` object supports UBL XML elements that don't have dedicated invoice fields. All sub-fields are optional. Unknown keys are silently stripped. Pass `null` to clear all extensions. | Name | Type | Description | |------|------|-------------| | `invoicePeriod` | object | Billing period: `startDate` (YYYY-MM-DD), `endDate` (YYYY-MM-DD), `descriptionCode` (e.g., "35") | | `delivery` | object | Delivery info: `actualDeliveryDate` (YYYY-MM-DD), `deliveryAddress` object with `streetName`, `cityName`, `countrySubentity`, `countryCode` | | `allowanceCharges` | array | Document-level allowances/charges (max 20). Each: `chargeIndicator` (bool, false=discount), `amount` (numeric string), `taxCategoryCode` (S/Z/E/AE/K/G/O), `taxRate` (numeric string). Optional: `reasonCode`, `reason`, `baseAmount`, `multiplierFactorNumeric` | | `prepaidAmount` | string | Prepaid amount (numeric string >= 0). Reduces PayableAmount in UBL XML | | `additionalDocumentReferences` | array | Additional references (max 10). Each: `id` (required, max 200), optional `documentTypeCode`, `documentDescription` | ### UBL extensions (line-level) Each line item can include a `ublExtensions` object: | Name | Type | Description | |------|------|-------------| | `invoicePeriod` | object | Line billing period: `startDate` (YYYY-MM-DD), `endDate` (YYYY-MM-DD) | | `allowanceCharges` | array | Line-level allowances/charges (max 10). Each: `chargeIndicator` (bool), `amount` (numeric string). Optional: `reasonCode`, `reason`, `baseAmount`, `multiplierFactorNumeric` | | `additionalItemProperties` | array | Item properties (max 20). Each: `name` (max 50 chars), `value` (max 100 chars) | | `originCountry` | string | Item origin country (ISO 3166-1 alpha-2, e.g., "DE") | When updating `lines`, the entire array is replaced. Include all line items you want to keep, not just the ones you're changing. ## Request ```bash curl -X PUT https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{ "dueDate": "2024-03-30", "notes": "Payment terms: 45 days net", "lines": [ { "description": "Web Development Services - Updated", "quantity": 15, "unitPrice": 100.00, "unitOfMeasure": "hours", "vatIncluded": false } ] }' ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7', { method: 'PUT', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000', 'Content-Type': 'application/json' }, body: JSON.stringify({ dueDate: '2024-03-30', notes: 'Payment terms: 45 days net', lines: [ { description: 'Web Development Services - Updated', quantity: 15, unitPrice: 100.00, unitOfMeasure: 'hours', vatIncluded: false } ] }) }); const data = await response.json(); // data.invoice — the updated invoice // data.validation — UBL validation results ``` ## Response Returns the updated invoice object along with UBL validation results. ```json { "invoice": { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "number": "FAC-2024-001", "status": "draft", "direction": "outgoing", "currency": "RON", "exchangeRate": 1.0, "issueDate": "2024-02-15", "dueDate": "2024-03-30", "subtotal": 1500.00, "vatTotal": 285.00, "total": 1785.00, "amountPaid": 0.00, "balance": 1785.00, "notes": "Payment terms: 45 days net", "lines": [ { "id": "2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e", "description": "Web Development Services - Updated", "quantity": 15.0, "unitPrice": 100.00, "unitOfMeasure": "hours", "vatRate": 19.0, "vatAmount": 285.00, "subtotal": 1500.00, "total": 1785.00 } ], "updatedAt": "2024-02-15T10:45:00Z" }, "validation": { "valid": true, "errors": [], "warnings": [], "schematronAvailable": true } } ``` ## Error codes | Code | Description | |------|-------------| | `400` | Validation error - invalid data format | | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Invoice not found | | `422` | Invoice is not editable (cancelled, sent to provider, or already uploaded to ANAF) | --- ## Validate invoice > Validate invoice data before issuing or submitting URL: https://docs.storno.ro/api-reference/invoices/validate # Validate invoice Validates invoice data against business rules and optionally against the full UBL Schematron validation rules. Use this endpoint to check for errors before issuing or submitting an invoice. ``` POST /api/v1/invoices/{uuid}/validate ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | ## Query parameters | Name | Type | Default | Description | |------|------|---------|-------------| | `mode` | string | quick | Validation mode: `quick` or `full` | ### Validation modes - **quick** - Fast validation against basic business rules and data integrity - **full** - Comprehensive validation including Schematron rules and UBL 2.1 schema compliance The `full` validation mode can take several seconds for complex invoices. Use `quick` mode for real-time validation feedback in the UI. ## Request ```bash # Quick validation curl -X POST "https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/validate?mode=quick" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" # Full validation curl -X POST "https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/validate?mode=full" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript // Quick validation const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/validate?mode=quick', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const validation = await response.json(); if (!validation.valid) { console.error('Validation errors:', validation.errors); } ``` ## Response Returns validation results with any errors or warnings found. ### Valid invoice response ```json { "valid": true, "errors": [], "warnings": [] } ``` ### Invalid invoice response ```json { "valid": false, "errors": [ "Client VAT number (CIF) is required for invoices over 1000 RON", "Invoice line 1: Unit price must be greater than zero", "Due date cannot be before issue date" ], "warnings": [ "No payment terms specified", "Internal note is very long (over 500 characters)" ] } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `valid` | boolean | Whether the invoice passes all validation checks | | `errors` | array | Critical validation errors that must be fixed | | `warnings` | array | Non-critical issues or suggestions | ## Validation rules ### Quick mode checks - Required fields are present (client, issue date, lines) - Numeric values are valid (positive amounts, valid percentages) - Date logic (due date after issue date) - Line item calculations are correct - VAT rates are valid - Currency code is valid - Client and supplier information is complete - Total amounts match line item sums ### Full mode additional checks - UBL 2.1 schema compliance - Schematron business rules (EN 16931) - Cross-field validation rules - Romanian e-Factura specific rules - Tax identification number formats - Address format validation - Payment terms codelist compliance - Unit of measure codelist compliance ## Common validation errors | Error | Description | Solution | |-------|-------------|----------| | Missing client CIF | VAT identification number required | Add client CIF/VAT number | | Invalid VAT rate | VAT rate not in allowed list | Use a valid VAT rate (0, 5, 9, 19) | | Negative amounts | Line price or quantity is negative | Use positive values | | Date mismatch | Due date before issue date | Adjust dates | | Missing lines | Invoice has no line items | Add at least one line | | Invalid currency | Currency code not ISO 4217 | Use valid 3-letter code (RON, EUR, USD) | | Total mismatch | Calculated total doesn't match line sums | Recalculate line items | ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Invoice not found | | `422` | Invalid validation mode parameter | ## Related endpoints - [Issue invoice](/api-reference/invoices/issue) - Issue invoice after validation passes - [Submit invoice](/api-reference/invoices/submit) - Submit validated invoice to e-invoice provider - [Update invoice](/api-reference/invoices/update) - Fix validation errors --- ## Verify ANAF signature > Verify the digital signature on an ANAF-validated invoice URL: https://docs.storno.ro/api-reference/invoices/verify-signature # Verify ANAF signature Verifies the digital signature applied by ANAF to a validated invoice. This confirms the authenticity and integrity of the invoice XML. ``` POST /api/v1/invoices/{uuid}/verify-signature ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Invoice UUID | This endpoint requires the invoice to have been submitted to and validated by ANAF. ## Prerequisites Before verifying a signature: 1. Invoice must be submitted to ANAF 2. Invoice must have `validated` status 3. ANAF must have applied a digital signature ## Request ```bash curl -X POST https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/verify-signature \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/invoices/7c9e6679-7425-40de-944b-e07fc1f90ae7/verify-signature', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const verification = await response.json(); if (verification.valid) { console.log('✓ Signature is valid'); console.log('Signed by:', verification.signer); } else { console.error('✗ Signature verification failed'); } ``` ## Response Returns signature verification results with signer details and timestamp. ### Valid signature response ```json { "valid": true, "signer": "ANAF - Agentia Nationala de Administrare Fiscala", "signerCertificate": { "subject": "CN=ANAF e-Factura, O=Ministerul Finantelor Publice, C=RO", "issuer": "CN=Root CA, O=Autoritatea Nationala de Certificare, C=RO", "serialNumber": "1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D", "validFrom": "2023-01-01T00:00:00Z", "validUntil": "2025-12-31T23:59:59Z", "fingerprint": "SHA256:1234567890ABCDEF..." }, "signatureTimestamp": "2024-02-15T10:00:00Z", "algorithm": "RSA-SHA256", "signatureFormat": "XMLDSig", "certificateChainValid": true, "timestampValid": true, "xmlIntegrityValid": true, "verifiedAt": "2024-02-16T15:00:00Z" } ``` ### Invalid signature response ```json { "valid": false, "error": "signature_mismatch", "errorMessage": "The XML content has been modified after signing", "signer": "ANAF - Agentia Nationala de Administrare Fiscala", "signatureTimestamp": "2024-02-15T10:00:00Z", "verifiedAt": "2024-02-16T15:00:00Z", "details": { "certificateChainValid": true, "timestampValid": true, "xmlIntegrityValid": false } } ``` ## Response fields | Field | Type | Description | |-------|------|-------------| | `valid` | boolean | Whether the signature is valid | | `signer` | string | Name of the signing authority | | `signerCertificate` | object | X.509 certificate details | | `signatureTimestamp` | string | When the signature was created | | `algorithm` | string | Signature algorithm used | | `signatureFormat` | string | Signature format (XMLDSig, XAdES) | | `certificateChainValid` | boolean | Certificate chain verification | | `timestampValid` | boolean | Timestamp verification | | `xmlIntegrityValid` | boolean | XML content integrity check | | `verifiedAt` | string | When verification was performed | ## Verification checks The signature verification process includes: ### 1. Certificate validation - Certificate is issued by trusted CA - Certificate is not expired - Certificate chain is complete and valid - Certificate has not been revoked ### 2. Signature validation - Signature cryptographically matches the XML - Signature algorithm is secure - Signature format conforms to standards ### 3. Content integrity - XML content has not been modified - All referenced elements are present - Hash values match original content ### 4. Timestamp validation - Timestamp is from trusted authority - Timestamp is within valid range - Timestamp matches signature creation ## Why verify signatures Digital signature verification is important for: - **Legal compliance** - Ensure invoice authenticity for audits - **Fraud prevention** - Detect tampered or forged invoices - **Dispute resolution** - Prove invoice integrity in legal disputes - **Archival integrity** - Verify archived invoices haven't been altered - **Third-party validation** - Allow clients to verify invoice authenticity ## Signature validity period ANAF signatures are typically valid for: - **Certificate validity** - 2-3 years from issue date - **Timestamp validity** - Permanent (as long as timestamping service is trusted) - **Archive validity** - 10+ years with qualified timestamp Signature verification may fail if performed after the signer certificate expires. Perform verification and archive results for long-term compliance. ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to company | | `404` | Invoice not found or not validated by ANAF | | `422` | No digital signature present on invoice | | `500` | Verification service temporarily unavailable | ## Common verification errors | Error code | Description | Solution | |------------|-------------|----------| | `no_signature` | Invoice has no digital signature | Ensure invoice was validated by ANAF | | `signature_mismatch` | XML was modified after signing | Re-download original from ANAF | | `certificate_expired` | Signer certificate has expired | Verification is no longer possible | | `certificate_revoked` | Certificate was revoked | Contact ANAF support | | `invalid_chain` | Certificate chain is broken | Check trusted CA certificates | | `timestamp_invalid` | Timestamp verification failed | Contact ANAF support | ## Related endpoints - [Submit to ANAF](/api-reference/invoices/submit) - Submit invoice to get signature - [Download XML](/api-reference/invoices/xml) - Download signed XML - [Invoice events](/api-reference/invoices/events) - View validation history - [Get invoice details](/api-reference/invoices/get) - Check validation status --- ## Create License Key > Generate a new license key for self-hosted instances. URL: https://docs.storno.ro/api-reference/licensing/create-key # Create License Key Generate a new license key for the current organization. The key can be used to configure a self-hosted Docker instance. Only the organization owner can create license keys. ## Request ### Headers | Header | Required | Description | |--------|----------|-------------| | `Authorization` | Yes | Bearer token | ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `instanceName` | string | No | Human-readable name for the instance (e.g., "Production", "Staging") | ### Example Request ```bash curl -X POST https://app.storno.ro/api/v1/licensing/keys \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "instanceName": "Production Server" }' ``` ```js const response = await fetch('https://app.storno.ro/api/v1/licensing/keys', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ instanceName: 'Production Server', }), }); const data = await response.json(); // data.licenseKey — save this! It's shown only once at full length. ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->post('https://app.storno.ro/api/v1/licensing/keys', [ 'headers' => [ 'Authorization' => 'Bearer ' . $token, ], 'json' => [ 'instanceName' => 'Production Server', ], ]); $data = json_decode($response->getBody(), true); // $data['licenseKey'] — save this! ``` ## Response ### Success Response (201 Created) ```json { "id": "019c8a12-4567-7abc-def0-123456789abc", "licenseKey": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", "instanceName": "Production Server", "active": true, "createdAt": "2026-02-20T10:30:00+00:00" } ``` | Field | Type | Description | |-------|------|-------------| | `id` | string | UUID of the license key | | `licenseKey` | string | The 64-character license key. **Save this immediately** — it is only returned in full on creation. Subsequent list requests show a masked version. | | `instanceName` | string | Instance label, if provided | | `active` | boolean | Whether the key is active | | `createdAt` | string | ISO 8601 creation timestamp | ## Error Codes | Code | Description | |------|-------------| | `401` | Unauthorized — Missing or invalid token | | `403` | Forbidden — User is not the organization owner | | `404` | Not Found — Organization not found | ## Usage Notes - **Only the organization owner** can create license keys - The full license key is returned **only at creation time**. Copy it immediately. - There is no limit on the number of keys per organization, but each self-hosted instance should use its own key - The key inherits the organization's current subscription plan --- ## List License Keys > List all license keys for the current organization. URL: https://docs.storno.ro/api-reference/licensing/list-keys # List License Keys Retrieve all license keys issued for the current organization. Keys are returned with masked values for security. Only the organization owner can list keys. ## Request ### Headers | Header | Required | Description | |--------|----------|-------------| | `Authorization` | Yes | Bearer token | ### Example Request ```bash curl https://app.storno.ro/api/v1/licensing/keys \ -H "Authorization: Bearer {token}" ``` ```js const response = await fetch('https://app.storno.ro/api/v1/licensing/keys', { headers: { 'Authorization': `Bearer ${token}`, }, }); const { keys } = await response.json(); ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->get('https://app.storno.ro/api/v1/licensing/keys', [ 'headers' => [ 'Authorization' => 'Bearer ' . $token, ], ]); $data = json_decode($response->getBody(), true); $keys = $data['keys']; ``` ## Response ### Success Response (200 OK) ```json { "keys": [ { "id": "019c8a12-4567-7abc-def0-123456789abc", "licenseKey": "a1b2c3d4...e5f6a1b2", "instanceName": "Production Server", "instanceUrl": "https://factura.mycompany.ro", "active": true, "lastValidatedAt": "2026-02-20T08:00:00+00:00", "activatedAt": "2026-02-01T12:00:00+00:00", "createdAt": "2026-02-01T10:30:00+00:00" }, { "id": "019c8b34-5678-7def-0123-456789abcdef", "licenseKey": "b2c3d4e5...f6a1b2c3", "instanceName": "Staging", "instanceUrl": null, "active": true, "lastValidatedAt": null, "activatedAt": null, "createdAt": "2026-02-15T14:00:00+00:00" } ] } ``` | Field | Type | Description | |-------|------|-------------| | `id` | string | UUID of the license key | | `licenseKey` | string | Masked key (first 8 + last 8 characters) | | `instanceName` | string\|null | Human-readable instance label | | `instanceUrl` | string\|null | URL reported by the self-hosted instance during validation | | `active` | boolean | Whether the key is active | | `lastValidatedAt` | string\|null | Last time this key was validated by a self-hosted instance | | `activatedAt` | string\|null | When the key was first used | | `createdAt` | string | ISO 8601 creation timestamp | ## Error Codes | Code | Description | |------|-------------| | `401` | Unauthorized — Missing or invalid token | | `403` | Forbidden — User is not the organization owner | | `404` | Not Found — Organization not found | ## Usage Notes - License keys are **masked** in list responses — only the first and last 8 characters are shown - Use `lastValidatedAt` to verify that a self-hosted instance is actively running - Keys with `lastValidatedAt: null` have been created but never used - Inactive keys (revoked) are still returned in the list with `active: false` --- ## Revoke License Key > Deactivate a license key to revoke access for a self-hosted instance. URL: https://docs.storno.ro/api-reference/licensing/revoke-key # Revoke License Key Deactivate a license key, revoking access for the associated self-hosted instance. The instance will fall back to the Community (free) plan on its next validation cycle. Only the organization owner can revoke keys. ## Request ### Headers | Header | Required | Description | |--------|----------|-------------| | `Authorization` | Yes | Bearer token | ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | string | Yes | UUID of the license key to revoke | ### Example Request ```bash curl -X DELETE https://app.storno.ro/api/v1/licensing/keys/019c8a12-4567-7abc-def0-123456789abc \ -H "Authorization: Bearer {token}" ``` ```js const response = await fetch( `https://app.storno.ro/api/v1/licensing/keys/${keyId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}`, }, } ); const data = await response.json(); ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->delete( 'https://app.storno.ro/api/v1/licensing/keys/' . $keyId, [ 'headers' => [ 'Authorization' => 'Bearer ' . $token, ], ] ); ``` ## Response ### Success Response (200 OK) ```json { "status": "revoked" } ``` ## Error Codes | Code | Description | |------|-------------| | `401` | Unauthorized — Missing or invalid token | | `403` | Forbidden — User is not the organization owner | | `404` | Not Found — License key not found or belongs to a different organization | ## Usage Notes - Revocation is **soft delete** — the key record is kept but marked as `active: false` - Since JWT licenses are validated offline, a revoked key continues to work until it expires. Generate a new key with a shorter expiration if immediate revocation is needed. - Revoked keys cannot be reactivated. Generate a new key instead. --- ## Validate License > Validate a self-hosted license key and retrieve the current plan and features. URL: https://docs.storno.ro/api-reference/licensing/validate # Validate License Validate a license key issued to a self-hosted instance. Returns the current plan, features, and subscription details. This endpoint is called automatically by self-hosted instances and does not require authentication — the license key itself is the credential. **No authentication required.** ## Request ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `licenseKey` | string | Yes | 64-character hex license key | | `instanceName` | string | No | Human-readable name for this instance | | `instanceUrl` | string | No | Public URL of the self-hosted instance | ### Example Request ```bash curl -X POST https://app.storno.ro/api/v1/licensing/validate \ -H "Content-Type: application/json" \ -d '{ "licenseKey": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", "instanceName": "Production Server", "instanceUrl": "https://factura.mycompany.ro" }' ``` ```js const response = await fetch('https://app.storno.ro/api/v1/licensing/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ licenseKey: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', instanceName: 'Production Server', instanceUrl: 'https://factura.mycompany.ro', }), }); const data = await response.json(); ``` ```php $client = new \GuzzleHttp\Client(); $response = $client->post('https://app.storno.ro/api/v1/licensing/validate', [ 'json' => [ 'licenseKey' => 'a1b2c3d4e5f6...64-char-key', 'instanceName' => 'Production Server', 'instanceUrl' => 'https://factura.mycompany.ro', ], ]); $data = json_decode($response->getBody(), true); ``` ## Response ### Success Response (200 OK) ```json { "valid": true, "plan": "pro", "features": { "maxCompanies": 5, "maxInvoicesPerMonth": 500, "maxUsers": 5, "recurringInvoices": true, "apiAccess": true, "prioritySupport": false }, "organizationName": "SC Firma Mea SRL", "currentPeriodEnd": "2026-03-20T00:00:00+00:00", "billingUrl": "https://app.storno.ro/settings/billing" } ``` | Field | Type | Description | |-------|------|-------------| | `valid` | boolean | Whether the license key is valid | | `plan` | string | Current plan: `free`, `pro`, or `business` | | `features` | object | Plan feature limits and flags | | `organizationName` | string | Name of the organization that owns this key | | `currentPeriodEnd` | string | ISO 8601 timestamp of the billing period end | | `trialEndsAt` | string | ISO 8601 timestamp (only present during trial) | | `trialDaysLeft` | integer | Days remaining in trial (only present during trial) | | `billingUrl` | string | URL to manage the subscription on the SaaS | ### Trial Response When the organization is in a trial period, additional fields are included: ```json { "valid": true, "plan": "pro", "features": { ... }, "organizationName": "SC Firma Mea SRL", "trialEndsAt": "2026-03-06T00:00:00+00:00", "trialDaysLeft": 14, "billingUrl": "https://app.storno.ro/settings/billing" } ``` ## Error Codes | Code | Description | |------|-------------| | `400` | Bad Request — `licenseKey` is missing | | `401` | Unauthorized — Invalid or revoked license key | | `403` | Forbidden — Organization is inactive | ### Error Response Examples **Missing Key (400)** ```json { "error": "licenseKey is required" } ``` **Invalid Key (401)** ```json { "valid": false, "error": "Invalid or revoked license key" } ``` **Inactive Organization (403)** ```json { "valid": false, "error": "Organization is inactive" } ``` ## Usage Notes - This endpoint is **unauthenticated** — the license key is the credential - Self-hosted instances should validate every **24 hours** (via `app:license:sync` cron) - The `instanceName` and `instanceUrl` are stored for identification purposes in the SaaS dashboard - JWT licenses are validated offline and do not use this endpoint - No user data or business information is sent in the request --- ## Delete Email Sender Configuration > Remove the organization's custom SMTP email sender URL: https://docs.storno.ro/api-reference/mailer-config/delete # Delete Email Sender Configuration Removes the organization's custom email sender. Client documents revert to being sent from the default Storno address. ``` DELETE /api/v1/mailer-config ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token or API key for authentication | Requires the `settings.manage` permission. ## Request ```bash {% title="cURL" %} curl -X DELETE https://api.storno.ro/api/v1/mailer-config \ -H "Authorization: Bearer YOUR_TOKEN" ``` ## Response ```json { "message": "Mailer configuration deleted." } ``` ## Error Codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | Missing `settings.manage` permission | | `404` | Organization not found, or no configuration exists | --- ## Get Email Sender Configuration > Retrieve the organization's custom SMTP email sender configuration URL: https://docs.storno.ro/api-reference/mailer-config/get # Get Email Sender Configuration Returns the organization's custom email sender (SMTP). Available on the **Business** plan; `entitled` reflects whether the current plan allows it. The SMTP password is never returned — `hasPassword` indicates whether one is stored. ``` GET /api/v1/mailer-config ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token or API key for authentication | Requires the `settings.view` permission. ## Request ```bash {% title="cURL" %} curl https://api.storno.ro/api/v1/mailer-config \ -H "Authorization: Bearer YOUR_TOKEN" ``` ## Response ```json { "entitled": true, "data": { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "enabled": true, "host": "smtp.example.com", "port": 587, "encryption": "tls", "username": "facturi@example.com", "fromAddress": "facturi@example.com", "fromName": "Acme SRL", "hasPassword": true, "lastTestedAt": "2026-06-02T10:15:00+00:00" } } ``` When no configuration exists, `data` is `null`. ## Response Fields | Field | Type | Description | |-------|------|-------------| | `entitled` | boolean | Whether the org's plan (Business) allows a custom sender | | `data` | object\|null | The configuration, or `null` if not yet created | | `data.enabled` | boolean | Whether the custom sender is active | | `data.host` | string | SMTP host | | `data.port` | integer | SMTP port | | `data.encryption` | string | `none`, `tls` (STARTTLS), or `ssl` (implicit) | | `data.username` | string\|null | SMTP username | | `data.fromAddress` | string | Sender address | | `data.fromName` | string\|null | Sender display name | | `data.hasPassword` | boolean | Whether an SMTP password is stored | | `data.lastTestedAt` | string\|null | ISO-8601 timestamp of the last successful test | ## Error Codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | Missing `settings.view` permission | | `404` | Organization not found | --- ## Test Email Sender > Send a test message through the custom SMTP sender URL: https://docs.storno.ro/api-reference/mailer-config/test # Test Email Sender Sends a test message through the custom SMTP sender to verify the connection and credentials. Uses the stored configuration unless overridden in the body. Requires the **Business** plan. A successful test updates `lastTestedAt` on the stored configuration. ``` POST /api/v1/mailer-config/test ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token or API key for authentication | | `Content-Type` | string | Yes | `application/json` | Requires the `settings.manage` permission. ## Body Parameters All optional — any omitted field falls back to the stored configuration. | Field | Type | Description | |-------|------|-------------| | `testEmail` | string | Recipient for the test message (defaults to the from address) | | `host` | string | Override SMTP host | | `port` | integer | Override SMTP port | | `encryption` | string | Override encryption (`none`, `tls`, `ssl`) | | `username` | string | Override SMTP username | | `password` | string | Override SMTP password | | `fromAddress` | string | Override sender address | | `fromName` | string | Override sender display name | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/mailer-config/test \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "testEmail": "me@example.com" }' ``` ## Response ```json { "success": true, "message": "Test email sent to me@example.com." } ``` On failure, `success` is `false` and `error` contains the SMTP error message. ## Error Codes | Code | Description | |------|-------------| | `400` | Missing host or invalid from address | | `401` | Missing or invalid authentication token | | `403` | Missing `settings.manage` permission, or plan is not Business | | `404` | Organization not found | --- ## Update Email Sender Configuration > Configure a custom SMTP email sender for the organization URL: https://docs.storno.ro/api-reference/mailer-config/update # Update Email Sender Configuration Creates or updates the organization's custom SMTP sender. Requires the **Business** plan; other plans return `403`. Once enabled, invoices, receipts, and delivery notes sent to clients go through this SMTP server from your own address. Omit `password` on update to keep the stored one. ``` PUT /api/v1/mailer-config ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token or API key for authentication | | `Content-Type` | string | Yes | `application/json` | Requires the `settings.manage` permission. ## Body Parameters | Field | Type | Required | Description | |-------|------|----------|-------------| | `host` | string | Yes (on create) | SMTP host | | `fromAddress` | string | Yes (on create) | Sender address, authorized on the SMTP server | | `password` | string | Yes (on create) | SMTP password (omit on update to keep the stored one) | | `port` | integer | No | SMTP port (default `587`) | | `encryption` | string | No | `none`, `tls` (STARTTLS, default), or `ssl` (implicit) | | `username` | string | No | SMTP username | | `fromName` | string | No | Sender display name | | `enabled` | boolean | No | Activate or deactivate the custom sender | ## Request ```bash {% title="cURL" %} curl -X PUT https://api.storno.ro/api/v1/mailer-config \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "enabled": true, "host": "smtp.example.com", "port": 587, "encryption": "tls", "username": "facturi@example.com", "password": "app-specific-password", "fromAddress": "facturi@example.com", "fromName": "Acme SRL" }' ``` ## Response Returns the saved configuration (without the password). See [Get Email Sender Configuration](/api-reference/mailer-config/get). ## Error Codes | Code | Description | |------|-------------| | `400` | Missing host, invalid from address, invalid encryption, or missing password on create | | `401` | Missing or invalid authentication token | | `403` | Missing `settings.manage` permission, or plan is not Business | | `404` | Organization not found | --- ## Deactivate Organization Member > Deactivate a member from the organization URL: https://docs.storno.ro/api-reference/members/delete # Deactivate Organization Member Deactivate a member from the organization. This prevents them from accessing the organization but preserves historical data. --- ## Deactivate Member ```http DELETE /api/v1/members/{uuid} ``` Deactivate a member by setting their `isActive` status to false. Member data is preserved for audit purposes. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | uuid | string | Member UUID | ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response Returns `204 No Content` on successful deactivation. ### Restrictions The following members cannot be deactivated: - The authenticated user (cannot deactivate self) - Organization owner - Super admin accounts ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - cannot deactivate self, owner, or super admin | | 404 | Member not found | ### Notes - Deactivation is a soft delete - data is preserved - Deactivated members cannot log in to the organization - Invoices and other data created by the member remain intact - To reactivate, use the PATCH endpoint to set `isActive: true` - Only organization admins and owners can deactivate members --- ## List Organization Members > List all members of the organization with permissions URL: https://docs.storno.ro/api-reference/members/list # List Organization Members Retrieve a list of all members in the organization with their roles and permissions. --- ## Get Members ```http GET /api/v1/members ``` Get all organization members with permission metadata. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response Returns an array of member objects. ```json [ { "uuid": "123e4567-e89b-12d3-a456-426614174000", "email": "john.doe@example.com", "firstName": "John", "lastName": "Doe", "role": "ADMIN", "isActive": true, "allowedCompanies": [ "550e8400-e29b-41d4-a716-446655440000", "660e8400-e29b-41d4-a716-446655440001" ], "joinedAt": "2026-01-15T10:00:00Z" }, { "uuid": "223e4567-e89b-12d3-a456-426614174001", "email": "jane.smith@example.com", "firstName": "Jane", "lastName": "Smith", "role": "ACCOUNTANT", "isActive": true, "allowedCompanies": [ "550e8400-e29b-41d4-a716-446655440000" ], "joinedAt": "2026-02-01T14:30:00Z" } ] ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | uuid | string | Unique member identifier | | email | string | Member email address | | firstName | string | Member first name | | lastName | string | Member last name | | role | string | Member role (see roles below) | | isActive | boolean | Whether member is active | | allowedCompanies | array | Array of company UUIDs member can access | | joinedAt | string | ISO 8601 timestamp when member joined | ### Role Types | Role | Description | Permissions | |------|-------------|-------------| | OWNER | Organization owner | Full access to all features and settings | | ADMIN | Administrator | Manage members, companies, settings (cannot delete owner) | | ACCOUNTANT | Accountant | Manage invoices, reports, sync (no member management) | | EMPLOYEE | Employee | View-only access to assigned companies | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - insufficient permissions | ### Notes - Only organization admins and owners can list members - The `allowedCompanies` array controls which companies a member can access - OWNER role members have access to all companies automatically --- ## Update Organization Member > Update member role and permissions URL: https://docs.storno.ro/api-reference/members/update # Update Organization Member Update a member's role, active status, and company access permissions. --- ## Update Member ```http PATCH /api/v1/members/{uuid} ``` Update member information including role, active status, and allowed companies. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | uuid | string | Member UUID | ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | | Content-Type | string | Yes | Must be `application/json` | ### Request Body ```json { "role": "ACCOUNTANT", "isActive": true, "allowedCompanies": [ "550e8400-e29b-41d4-a716-446655440000" ] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | role | string | No | Member role: `ADMIN`, `ACCOUNTANT`, or `EMPLOYEE` | | isActive | boolean | No | Whether member is active | | allowedCompanies | array | No | Array of company UUIDs | ### Response Returns the updated member object: ```json { "uuid": "223e4567-e89b-12d3-a456-426614174001", "email": "jane.smith@example.com", "firstName": "Jane", "lastName": "Smith", "role": "ACCOUNTANT", "isActive": true, "allowedCompanies": [ "550e8400-e29b-41d4-a716-446655440000" ], "joinedAt": "2026-02-01T14:30:00Z" } ``` ### Restrictions - Cannot change a member's role to `OWNER` - Cannot modify the organization owner - Cannot modify super admin accounts - Only admins and owners can update members - Must provide at least one allowed company for active members ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - insufficient permissions or attempting protected action | | 404 | Member not found | | 422 | Validation error - invalid role or empty allowed companies | ### Notes - OWNER role can only be transferred through organization ownership transfer - Deactivating a member prevents login but preserves data - Use DELETE endpoint to fully deactivate a member --- ## Get Notification Preferences > Retrieve user notification preferences URL: https://docs.storno.ro/api-reference/notification-preferences/get # Get Notification Preferences Retrieve notification preferences for the authenticated user. --- ## Get Preferences ```http GET /api/v1/notification-preferences ``` Get the user's notification preferences including channel preferences for each notification type. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response ```json { "preferences": { "invoiceReceived": { "email": true, "inApp": true, "push": true }, "invoicePaid": { "email": true, "inApp": true, "push": false }, "syncCompleted": { "email": false, "inApp": true, "push": false }, "syncFailed": { "email": true, "inApp": true, "push": true }, "tokenExpiring": { "email": true, "inApp": true, "push": true }, "tokenExpired": { "email": true, "inApp": true, "push": true }, "paymentOverdue": { "email": true, "inApp": true, "push": true }, "invitationReceived": { "email": true, "inApp": true, "push": false } } } ``` ### Response Fields Each notification type has three channel preferences: | Field | Type | Description | |-------|------|-------------| | email | boolean | Send email notifications | | inApp | boolean | Show in-app notifications | | push | boolean | Send push notifications | ### Notification Types | Type | Description | |------|-------------| | invoiceReceived | New invoice received from ANAF | | invoicePaid | Invoice marked as paid | | syncCompleted | E-Factura sync finished successfully | | syncFailed | E-Factura sync failed | | tokenExpiring | ANAF token expiring within 7 days | | tokenExpired | ANAF token has expired | | paymentOverdue | Invoice payment is overdue | | invitationReceived | Invited to join organization | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | ### Notes - Default preferences are applied for new users - Preferences are stored per user, not per organization --- ## Update Notification Preferences > Update user notification preferences URL: https://docs.storno.ro/api-reference/notification-preferences/update # Update Notification Preferences Update notification preferences for the authenticated user. --- ## Update Preferences ```http PUT /api/v1/notification-preferences ``` Update the user's notification preferences. All notification types must be provided in the request. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | | Content-Type | string | Yes | Must be `application/json` | ### Request Body ```json { "preferences": { "invoiceReceived": { "email": true, "inApp": true, "push": false }, "invoicePaid": { "email": false, "inApp": true, "push": false }, "syncCompleted": { "email": false, "inApp": true, "push": false }, "syncFailed": { "email": true, "inApp": true, "push": true }, "tokenExpiring": { "email": true, "inApp": true, "push": true }, "tokenExpired": { "email": true, "inApp": true, "push": true }, "paymentOverdue": { "email": true, "inApp": true, "push": true }, "invitationReceived": { "email": true, "inApp": true, "push": false } } } ``` ### Request Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | preferences | object | Yes | Notification preferences object | Each notification type must include: | Field | Type | Required | Description | |-------|------|----------|-------------| | email | boolean | Yes | Enable email notifications | | inApp | boolean | Yes | Enable in-app notifications | | push | boolean | Yes | Enable push notifications | ### Response Returns the updated preferences (same format as GET endpoint): ```json { "preferences": { "invoiceReceived": { "email": true, "inApp": true, "push": false } // ... other preferences } } ``` ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 422 | Validation error - missing or invalid preferences | ### Notes - All notification types must be provided - Partial updates are not supported - use PUT with full object - Changes take effect immediately --- ## List Notifications > Retrieve paginated list of user notifications URL: https://docs.storno.ro/api-reference/notifications/list # List Notifications Retrieve a paginated list of notifications for the authenticated user. --- ## Get Notifications ```http GET /api/v1/notifications ``` Retrieve user notifications with pagination support. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | page | integer | No | Page number (default: 1) | | limit | integer | No | Items per page (default: 20, max: 100) | ### Response ```json { "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "type": "invoice_received", "title": "Factură nouă primită", "message": "Ați primit factura FINV2026001 de la SC Example SRL", "data": { "invoiceId": "123e4567-e89b-12d3-a456-426614174000", "invoiceNumber": "FINV2026001", "clientName": "SC Example SRL", "amount": 11900.00 }, "isRead": false, "createdAt": "2026-02-16T10:30:00Z", "push": { "attempted": true, "sentAt": "2026-02-16T10:30:01Z", "error": null, "skippedReason": null } }, { "id": "660e8400-e29b-41d4-a716-446655440001", "type": "sync_completed", "title": "Sincronizare finalizată", "message": "Sincronizare e-Factura finalizată: 5 facturi noi", "data": { "syncedCount": 5, "cif": "12345678" }, "isRead": true, "createdAt": "2026-02-16T09:00:00Z", "push": { "attempted": false, "sentAt": null, "error": null, "skippedReason": "quiet_hours" } } ], "total": 47, "page": 1, "limit": 20, "pages": 3 } ``` ### Response Fields #### Pagination | Field | Type | Description | |-------|------|-------------| | data | array | Array of notification objects | | total | integer | Total number of notifications | | page | integer | Current page number | | limit | integer | Items per page | | pages | integer | Total number of pages | #### Notification Object | Field | Type | Description | |-------|------|-------------| | id | string | UUID of notification | | type | string | Notification type identifier | | title | string | Notification title | | message | string | Notification message | | data | object | Additional notification data (type-specific) | | isRead | boolean | Whether notification has been read | | createdAt | string | ISO 8601 timestamp | | push | object | Push delivery diagnostics (see below) | #### Push Delivery Object | Field | Type | Description | |-------|------|-------------| | attempted | boolean | Whether the backend tried to push to FCM/APNs | | sentAt | string\|null | ISO 8601 timestamp of the first successful device delivery | | error | string\|null | Last error from FCM/relay if all attempts failed | | skippedReason | string\|null | `quiet_hours` (muted overnight) or `no_devices` (no registered devices) | ### Notification Types | Type | Description | |------|-------------| | invoice_received | New invoice received from ANAF | | invoice_paid | Invoice marked as paid | | sync_completed | E-Factura sync finished | | sync_failed | E-Factura sync failed | | token_expiring | ANAF token expiring soon | | token_expired | ANAF token has expired | | payment_overdue | Payment is overdue | | invitation_received | Invited to join organization | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 422 | Invalid pagination parameters | --- ## Mark All Notifications as Read > Mark all notifications as read at once URL: https://docs.storno.ro/api-reference/notifications/read-all # Mark All Notifications as Read Mark all notifications for the authenticated user as read in a single operation. --- ## Mark All as Read ```http POST /api/v1/notifications/read-all ``` Mark all user notifications as read. This is useful for "clear all" functionality in notification panels. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response Returns `204 No Content` on success. ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | ### Notes - This operation is idempotent - All notifications will have `isRead` set to `true` - The unread count will be reset to 0 --- ## Mark Notification as Read > Mark a specific notification as read URL: https://docs.storno.ro/api-reference/notifications/read # Mark Notification as Read Mark a specific notification as read for the authenticated user. --- ## Mark as Read ```http PATCH /api/v1/notifications/{id}/read ``` Mark a single notification as read. This is typically called when the user views the notification. ### Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | id | string | Notification UUID | ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response Returns `204 No Content` on success. ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - notification belongs to another user | | 404 | Notification not found | ### Notes - This operation is idempotent - Marking as read decrements the unread count - Already-read notifications can be marked as read again without error --- ## Unread Notification Count > Get count of unread notifications URL: https://docs.storno.ro/api-reference/notifications/unread-count # Unread Notification Count Get the number of unread notifications for the authenticated user. --- ## Get Unread Count ```http GET /api/v1/notifications/unread-count ``` Retrieve the count of unread notifications. This endpoint is optimized for frequent polling (e.g., for notification badge). ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | ### Response ```json { "count": 5 } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | count | integer | Number of unread notifications | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | ### Notes - This endpoint is lightweight and suitable for polling - Consider using WebSocket/Centrifugo for real-time updates instead of frequent polling - Notifications are marked as read via the `/notifications/{id}/read` endpoint --- ## Authorization endpoint > Fetch consent screen data and process user approval or denial of an OAuth2 authorization request. URL: https://docs.storno.ro/api-reference/oauth2/authorize # Authorization endpoint The authorization endpoint handles the interactive consent step of the OAuth2 Authorization Code flow. It exposes two methods on the same path: a `GET` to retrieve the client details needed to render the consent screen, and a `POST` to submit the user's decision. Both methods require an authenticated browser session (JWT). If the user is not authenticated, they must log in first before being redirected here. --- ## GET — Fetch consent screen data Returns the OAuth2 client's display information and the requested scopes so that your frontend can render a consent screen for the user. ```http GET /api/v1/oauth2/authorize ``` ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `response_type` | string | Yes | Must be `code` | | `client_id` | string | Yes | The client's public identifier, prefixed with `storno_cid_` | | `redirect_uri` | string | Yes | The URI to redirect to after authorization. Must exactly match one of the client's registered redirect URIs | | `scope` | string | Yes | Space-separated list of requested permission scopes | | `state` | string | Yes | A random, opaque value generated by your application to prevent CSRF attacks. Returned unchanged in the redirect | | `code_challenge` | string | Yes | The PKCE code challenge: `BASE64URL(SHA-256(ASCII(code_verifier)))` | | `code_challenge_method` | string | Yes | Must be `S256` | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer JWT from an active browser session | ### Response Returns `200 OK` with the client's display information and the list of requested scopes. | Field | Type | Description | |-------|------|-------------| | `clientName` | string | Display name of the OAuth2 application | | `clientLogoUrl` | string \| null | Logo URL of the application, or `null` if not set | | `clientWebsiteUrl` | string \| null | Website URL of the application, or `null` if not set | | `requestedScopes` | string[] | Array of scope values the application is requesting | ### Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/oauth2/authorize?response_type=code&client_id=storno_cid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6&redirect_uri=https%3A%2F%2Facme-accounting.com%2Foauth%2Fcallback&scope=invoice.view%20client.view&state=abc123&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256' \ -H 'Authorization: Bearer YOUR_JWT_TOKEN' ``` ### Example Response ```json { "clientName": "Acme Accounting Integration", "clientLogoUrl": "https://acme-accounting.com/logo.png", "clientWebsiteUrl": "https://acme-accounting.com", "requestedScopes": ["invoice.view", "client.view"] } ``` ### Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | invalid_request | Missing or invalid query parameters | | 401 | unauthorized | The user is not authenticated | | 404 | not_found | No active OAuth2 application found for the provided `client_id` | --- ## POST — Submit user decision Processes the user's approval or denial of the authorization request. Returns a redirect URI that your frontend should redirect the user to. ```http POST /api/v1/oauth2/authorize ``` ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer JWT from an active browser session | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `client_id` | string | Yes | The client's public identifier, prefixed with `storno_cid_` | | `redirect_uri` | string | Yes | Must exactly match one of the client's registered redirect URIs and the value used in the `GET` request | | `scope` | string | Yes | Space-separated list of scopes being authorized (may be a subset of what was originally requested) | | `state` | string | Yes | The `state` value from the original `GET` request, returned unchanged in the redirect | | `code_challenge` | string | Yes | The PKCE code challenge from the original `GET` request | | `code_challenge_method` | string | Yes | Must be `S256` | | `approved` | boolean | Yes | `true` if the user approved the request, `false` if they denied it | ### Response Returns `200 OK` with a `redirect_uri` string that your application must use to redirect the browser. **On approval:** ```json { "redirect_uri": "https://acme-accounting.com/oauth/callback?code=&state=abc123" } ``` **On denial:** ```json { "redirect_uri": "https://acme-accounting.com/oauth/callback?error=access_denied&state=abc123" } ``` | Field | Type | Description | |-------|------|-------------| | `redirect_uri` | string | The URI to redirect the user to. Contains either a `code` and `state` on approval, or an `error` and `state` on denial | ### Example Request (Approved) ```bash curl -X POST 'https://api.storno.ro/api/v1/oauth2/authorize' \ -H 'Authorization: Bearer YOUR_JWT_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "client_id": "storno_cid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "redirect_uri": "https://acme-accounting.com/oauth/callback", "scope": "invoice.view client.view", "state": "abc123", "code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", "code_challenge_method": "S256", "approved": true }' ``` ### Example Response (Approved) ```json { "redirect_uri": "https://acme-accounting.com/oauth/callback?code=4%2F0AfJohXlQs8kKpM7nNrZhQ2vWxyz&state=abc123" } ``` ### Example Request (Denied) ```bash curl -X POST 'https://api.storno.ro/api/v1/oauth2/authorize' \ -H 'Authorization: Bearer YOUR_JWT_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "client_id": "storno_cid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "redirect_uri": "https://acme-accounting.com/oauth/callback", "scope": "invoice.view client.view", "state": "abc123", "code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", "code_challenge_method": "S256", "approved": false }' ``` ### Example Response (Denied) ```json { "redirect_uri": "https://acme-accounting.com/oauth/callback?error=access_denied&state=abc123" } ``` ### Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | invalid_request | Missing or invalid body parameters, or `redirect_uri` does not match a registered URI | | 401 | unauthorized | The user is not authenticated | | 404 | not_found | No active OAuth2 application found for the provided `client_id` | | 422 | validation_error | Requested scopes are not a subset of the client's registered scopes, or the user does not hold the requested permissions | --- ## Important Notes - The `state` parameter must be validated by your application when handling the redirect — compare the value in the redirect query string against the value you generated before initiating the flow to prevent CSRF attacks - Authorization codes are single-use and expire after **10 minutes**. Exchange the code for tokens using the [Token endpoint](/api-reference/oauth2/token) promptly - The user may grant a narrower set of scopes than what was requested. The authorization code and resulting access token will reflect only the scopes the user approved - `code_challenge_method=S256` is the only supported method. `plain` is not accepted ## Related Endpoints - [Token endpoint](/api-reference/oauth2/token) - [Revoke token](/api-reference/oauth2/revoke-token) - [OAuth2 Provider overview](/api-reference/oauth2/overview) --- ## Create OAuth2 client > Register a new OAuth2 application to build a third-party integration with Storno. URL: https://docs.storno.ro/api-reference/oauth2/create-client # Create OAuth2 client Registers a new OAuth2 application for the current organization. The `clientSecret` is returned **only once** in the creation response and cannot be retrieved again — store it securely immediately after creation. For `public` clients, no `clientSecret` is issued; PKCE is the security mechanism. This endpoint requires an active browser session (JWT). It cannot be called using an API token or an OAuth2 access token. ```http POST /api/v1/oauth2/clients ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer JWT from an active browser session | | `Content-Type` | string | Yes | Must be `application/json` | ### Required permission `oauth2_app.manage` ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `name` | string | Yes | Human-readable display name for the application (shown on the consent screen) | | `clientType` | string | No | Either `confidential` or `public`. Defaults to `confidential` | | `redirectUris` | string[] | Yes | One or more URIs to which Storno may redirect after authorization. Must be valid HTTPS URLs; `http://localhost` and custom URI schemes are also accepted for development | | `scopes` | string[] | Yes | Permission scopes the application is allowed to request. Must be valid `Permission` values and a subset of the authenticated user's own permissions | | `description` | string | No | Optional description of the application's purpose | | `websiteUrl` | string | No | URL of the application's website, displayed on the consent screen | | `logoUrl` | string | No | URL of the application's logo image, displayed on the consent screen | ## Response Returns the created client object with a `201 Created` status. For `confidential` clients, the response includes a `clientSecret` field containing the raw secret value. This field is **not** included in any subsequent response. ### Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier (UUID) | | `name` | string | Display name of the application | | `description` | string \| null | Application description | | `clientId` | string | Public client identifier, prefixed with `storno_cid_` | | `clientSecret` | string \| null | The full raw client secret — present only for `confidential` clients, store this securely, it will not be shown again | | `clientSecretPrefix` | string \| null | First characters of the client secret for future identification. `null` for `public` clients | | `clientType` | string | `confidential` or `public` | | `redirectUris` | string[] | Registered redirect URIs | | `scopes` | string[] | Permitted scopes | | `websiteUrl` | string \| null | Application website URL | | `logoUrl` | string \| null | Application logo URL | | `isActive` | boolean | Always `true` on creation | | `revokedAt` | string \| null | Always `null` on creation | | `createdAt` | string | ISO 8601 creation timestamp | ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/oauth2/clients' \ -H 'Authorization: Bearer YOUR_JWT_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "Acme Accounting Integration", "clientType": "confidential", "redirectUris": ["https://acme-accounting.com/oauth/callback"], "scopes": ["invoice.view", "client.view"], "description": "Syncs invoices from Storno to Acme Accounting in real time.", "websiteUrl": "https://acme-accounting.com", "logoUrl": "https://acme-accounting.com/logo.png" }' ``` ## Example Response ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Acme Accounting Integration", "description": "Syncs invoices from Storno to Acme Accounting in real time.", "clientId": "storno_cid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "clientSecret": "storno_cs_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "clientSecretPrefix": "storno_cs_a1b2", "clientType": "confidential", "redirectUris": ["https://acme-accounting.com/oauth/callback"], "scopes": ["invoice.view", "client.view"], "websiteUrl": "https://acme-accounting.com", "logoUrl": "https://acme-accounting.com/logo.png", "isActive": true, "revokedAt": null, "createdAt": "2026-01-15T09:00:00Z" } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token, or token is not a browser session JWT | | 403 | forbidden | The authenticated user does not have the `oauth2_app.manage` permission | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - Missing `name` field - Missing or empty `redirectUris` array - One or more `redirectUris` are not valid URLs - Missing or empty `scopes` array - One or more scope values are not valid `Permission` values - One or more scopes exceed the authenticated user's own permissions - `clientType` is not `confidential` or `public` ## Important Notes - The `clientSecret` in the response is the only time the raw secret value is ever transmitted — it is stored as a one-way hash server-side - If you lose the `clientSecret`, use [Rotate client secret](/api-reference/oauth2/rotate-secret) to generate a new one; the old secret is immediately invalidated - `localhost` redirect URIs and custom URI schemes (e.g. `com.example.app://callback`) are accepted to support local development and native mobile applications - The requested `scopes` define the maximum scopes the application may ask for during an authorization flow — users may still grant a narrower subset on the consent screen - This endpoint requires a browser session JWT. API tokens and OAuth2 access tokens are not accepted, preventing automated creation of OAuth2 applications ## Related Endpoints - [List OAuth2 clients](/api-reference/oauth2/list-clients) - [Get OAuth2 client](/api-reference/oauth2/get-client) - [Update OAuth2 client](/api-reference/oauth2/update-client) - [Revoke OAuth2 client](/api-reference/oauth2/revoke-client) - [Rotate client secret](/api-reference/oauth2/rotate-secret) --- ## Get OAuth2 client > Retrieve a single OAuth2 application by UUID. URL: https://docs.storno.ro/api-reference/oauth2/get-client # Get OAuth2 client Returns a single OAuth2 application belonging to the current organization, identified by its UUID. The `clientSecret` value is never included in this response. ```http GET /api/v1/oauth2/clients/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the OAuth2 application to retrieve | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Organization` | string | No | Organization UUID. Defaults to the authenticated user's active organization | ### Required permission `oauth2_app.view` ## Response Returns the OAuth2Client object with a `200 OK` status. ### Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier (UUID) | | `name` | string | Human-readable display name of the application | | `description` | string \| null | Optional description of the application's purpose | | `clientId` | string | Public client identifier, prefixed with `storno_cid_` | | `clientSecretPrefix` | string \| null | First characters of the client secret for identification. `null` for `public` clients | | `clientType` | string | `confidential` or `public` | | `redirectUris` | string[] | Registered redirect URIs | | `scopes` | string[] | Permission scopes the application is allowed to request | | `websiteUrl` | string \| null | Application website URL | | `logoUrl` | string \| null | Application logo URL | | `isActive` | boolean | Whether the application can currently initiate new authorization flows | | `revokedAt` | string \| null | ISO 8601 timestamp of revocation, or `null` if still active | | `createdAt` | string | ISO 8601 creation timestamp | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/oauth2/clients/a1b2c3d4-e5f6-7890-abcd-ef1234567890' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` ## Example Response ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Acme Accounting Integration", "description": "Syncs invoices from Storno to Acme Accounting in real time.", "clientId": "storno_cid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "clientSecretPrefix": "storno_cs_a1b2", "clientType": "confidential", "redirectUris": ["https://acme-accounting.com/oauth/callback"], "scopes": ["invoice.view", "client.view"], "websiteUrl": "https://acme-accounting.com", "logoUrl": "https://acme-accounting.com/logo.png", "isActive": true, "revokedAt": null, "createdAt": "2026-01-15T09:00:00Z" } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | The authenticated user does not have the `oauth2_app.view` permission | | 404 | not_found | OAuth2 application not found, or belongs to a different organization | ## Important Notes - The full `clientSecret` is never returned by this endpoint — only `clientSecretPrefix` is exposed for identification - Revoked applications are still retrievable via this endpoint; check `revokedAt` to determine the application's status ## Related Endpoints - [List OAuth2 clients](/api-reference/oauth2/list-clients) - [Create OAuth2 client](/api-reference/oauth2/create-client) - [Update OAuth2 client](/api-reference/oauth2/update-client) - [Revoke OAuth2 client](/api-reference/oauth2/revoke-client) - [Rotate client secret](/api-reference/oauth2/rotate-secret) --- ## List OAuth2 clients > Retrieve all registered OAuth2 applications for the current organization. URL: https://docs.storno.ro/api-reference/oauth2/list-clients # List OAuth2 clients Returns all OAuth2 applications registered by the current organization. Results are sorted by creation date, newest first. The `clientSecret` value is never included in list or detail responses — it is only returned once at creation time or after a secret rotation. ```http GET /api/v1/oauth2/clients ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Organization` | string | No | Organization UUID. Defaults to the authenticated user's active organization | ### Required permission `oauth2_app.view` ## Response Returns a `{ data: [...] }` object containing an array of OAuth2Client objects. ### OAuth2Client object | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier (UUID) | | `name` | string | Human-readable display name of the application | | `description` | string \| null | Optional longer description of the application's purpose | | `clientId` | string | Public client identifier, prefixed with `storno_cid_` | | `clientSecretPrefix` | string | First characters of the client secret, used for identification without exposing the full value | | `clientType` | string | Either `confidential` or `public` | | `redirectUris` | string[] | Registered redirect URIs allowed during the authorization flow | | `scopes` | string[] | Permission scopes the application is allowed to request | | `websiteUrl` | string \| null | URL of the application's website, shown on the consent screen | | `logoUrl` | string \| null | URL of the application's logo image, shown on the consent screen | | `isActive` | boolean | Whether the application can currently initiate new authorization flows | | `revokedAt` | string \| null | ISO 8601 timestamp of when the application was revoked, or `null` if still active | | `createdAt` | string | ISO 8601 creation timestamp | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/oauth2/clients' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` ## Example Response ```json { "data": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Acme Accounting Integration", "description": "Syncs invoices from Storno to Acme Accounting in real time.", "clientId": "storno_cid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "clientSecretPrefix": "storno_cs_a1b2", "clientType": "confidential", "redirectUris": ["https://acme-accounting.com/oauth/callback"], "scopes": ["invoice.view", "client.view"], "websiteUrl": "https://acme-accounting.com", "logoUrl": "https://acme-accounting.com/logo.png", "isActive": true, "revokedAt": null, "createdAt": "2026-01-15T09:00:00Z" }, { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "name": "Mobile Expense Tracker", "description": null, "clientId": "storno_cid_b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7", "clientSecretPrefix": "storno_cs_b2c3", "clientType": "public", "redirectUris": ["com.example.expensetracker://oauth/callback"], "scopes": ["invoice.view", "export.data"], "websiteUrl": null, "logoUrl": null, "isActive": true, "revokedAt": null, "createdAt": "2025-11-20T14:30:00Z" } ] } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | The authenticated user does not have the `oauth2_app.view` permission | ## Important Notes - Both active and revoked applications are returned; filter on `revokedAt` or `isActive` to show only active clients - The full `clientSecret` value is never exposed after creation — only `clientSecretPrefix` is included for identification - Applications belong to the organization, not to an individual user; all members with the appropriate permission can view them ## Related Endpoints - [Create OAuth2 client](/api-reference/oauth2/create-client) - [Get OAuth2 client](/api-reference/oauth2/get-client) - [Update OAuth2 client](/api-reference/oauth2/update-client) - [Revoke OAuth2 client](/api-reference/oauth2/revoke-client) - [Rotate client secret](/api-reference/oauth2/rotate-secret) --- ## OAuth2 Provider > Use Storno as an OAuth2 authorization server to build third-party integrations that act on behalf of users. URL: https://docs.storno.ro/api-reference/oauth2/overview # OAuth2 Provider Storno implements the OAuth2 Authorization Code flow with mandatory PKCE (Proof Key for Code Exchange) support. Third-party applications can request delegated access to a user's Storno account with explicit user consent, without ever handling the user's password. ## When to use OAuth2 vs API tokens | Scenario | Recommended approach | |----------|----------------------| | You are building an integration used by many different Storno users | OAuth2 | | You are scripting against your own account | API token | | You need the user to explicitly approve a set of permissions | OAuth2 | | You need long-lived programmatic access with no user interaction | API token | ## Authorization Code + PKCE flow ``` 1. Your app generates a code_verifier and code_challenge (S256) 2. Your app redirects the user to Storno's authorization page 3. The user reviews the requested scopes and approves or denies 4. Storno redirects back to your redirect_uri with an authorization code 5. Your app exchanges the code for an access token and refresh token 6. Your app calls Storno API endpoints using the access token as a Bearer token 7. When the access token expires, your app exchanges the refresh token for a new pair ``` ### Step-by-step **1. Generate PKCE parameters** Generate a cryptographically random `code_verifier` (43–128 URL-safe characters) and compute the `code_challenge`: ``` code_challenge = BASE64URL(SHA-256(ASCII(code_verifier))) ``` **2. Redirect the user to the consent screen** ``` GET https://app.storno.ro/oauth2/authorize ?response_type=code &client_id=storno_cid_a1b2c3d4e5f6... &redirect_uri=https://yourapp.com/oauth/callback &scope=invoice.view%20client.view &state=random_csrf_token &code_challenge= &code_challenge_method=S256 ``` **3. Handle the redirect callback** On approval, Storno redirects to: ``` https://yourapp.com/oauth/callback?code=&state= ``` On denial: ``` https://yourapp.com/oauth/callback?error=access_denied&state= ``` **4. Exchange the code for tokens** ```bash curl -X POST 'https://api.storno.ro/api/v1/oauth2/token' \ -H 'Content-Type: application/json' \ -d '{ "grant_type": "authorization_code", "code": "", "redirect_uri": "https://yourapp.com/oauth/callback", "client_id": "storno_cid_a1b2c3d4e5f6...", "client_secret": "storno_cs_...", "code_verifier": "" }' ``` **5. Call API endpoints with the access token** ```bash curl 'https://api.storno.ro/api/v1/invoices' \ -H 'Authorization: Bearer storno_oat_...' ``` **6. Refresh when the access token expires** ```bash curl -X POST 'https://api.storno.ro/api/v1/oauth2/token' \ -H 'Content-Type: application/json' \ -d '{ "grant_type": "refresh_token", "refresh_token": "storno_ort_...", "client_id": "storno_cid_a1b2c3d4e5f6...", "client_secret": "storno_cs_..." }' ``` ## Token prefixes | Token | Prefix | Description | |-------|--------|-------------| | Access token | `storno_oat_` | Short-lived token for API calls | | Refresh token | `storno_ort_` | Long-lived token used to obtain new access tokens | | Client ID | `storno_cid_` | Public identifier for your OAuth2 application | | Client secret | `storno_cs_` | Secret credential for confidential clients | ## Token lifetimes | Token | Lifetime | |-------|----------| | Authorization code | 10 minutes | | Access token | 1 hour | | Refresh token | 30 days | ## Scopes OAuth2 scopes reuse the same `Permission` values used by API tokens. A user can only grant scopes that they themselves hold — the consent screen will only show scopes the authorizing user has. For the full scope list see [List available scopes](/api-reference/api-keys/scopes). Common scopes: | Scope | Description | |-------|-------------| | `invoice.view` | Read invoices | | `invoice.create` | Create new invoices | | `invoice.edit` | Edit existing invoices | | `client.view` | Read clients | | `client.create` | Create new clients | | `export.data` | Export data | ## Client types | Type | Description | |------|-------------| | `confidential` | Server-side applications that can securely store a client secret | | `public` | Client-side or mobile applications that cannot keep a secret (PKCE is the security mechanism) | ## Security model - **PKCE is mandatory** for all clients. `S256` is the only supported `code_challenge_method` - Tokens are stored as SHA-256 hashes server-side — the raw value is only transmitted once - Refresh tokens are rotated on every use. If a revoked refresh token is replayed, the entire token family is immediately revoked - Authorization codes are single-use and expire after 10 minutes - The token exchange endpoint (`/api/v1/oauth2/token`) is rate-limited to 20 requests per minute per client ## Related endpoints - [List OAuth2 clients](/api-reference/oauth2/list-clients) - [Create OAuth2 client](/api-reference/oauth2/create-client) - [Get OAuth2 client](/api-reference/oauth2/get-client) - [Update OAuth2 client](/api-reference/oauth2/update-client) - [Revoke OAuth2 client](/api-reference/oauth2/revoke-client) - [Rotate client secret](/api-reference/oauth2/rotate-secret) - [Authorization endpoint](/api-reference/oauth2/authorize) - [Token endpoint](/api-reference/oauth2/token) - [Revoke token](/api-reference/oauth2/revoke-token) --- ## Revoke OAuth2 client > Permanently revoke an OAuth2 application and all of its issued access and refresh tokens. URL: https://docs.storno.ro/api-reference/oauth2/revoke-client # Revoke OAuth2 client Permanently revokes an OAuth2 application and **all** access tokens and refresh tokens that were issued under it. All affected tokens stop working immediately. This operation cannot be undone — create a new application if access needs to be re-established. This endpoint requires an active browser session (JWT). It cannot be called using an API token or an OAuth2 access token. ```http DELETE /api/v1/oauth2/clients/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the OAuth2 application to revoke | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer JWT from an active browser session | ### Required permission `oauth2_app.manage` ## Response Returns a `204 No Content` status with no response body on success. ## Example Request ```bash curl -X DELETE 'https://api.storno.ro/api/v1/oauth2/clients/a1b2c3d4-e5f6-7890-abcd-ef1234567890' \ -H 'Authorization: Bearer YOUR_JWT_TOKEN' ``` ## Example Response ```http HTTP/1.1 204 No Content ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token, or token is not a browser session JWT | | 403 | forbidden | The authenticated user does not have the `oauth2_app.manage` permission | | 404 | not_found | OAuth2 application not found, or belongs to a different organization | ## Important Notes - Revocation is immediate and cascades to **all** tokens — every access token and refresh token ever issued for this application stops working at the moment this call completes - Third-party applications that currently hold valid tokens for this client will receive `401 Unauthorized` on their next API call - Revocation is permanent; there is no unrevoke operation. Create a new OAuth2 application and have users re-authorize if access is needed again - Revoking an already-revoked application returns `204 No Content` without error, making this operation idempotent - This endpoint requires a browser session JWT. API tokens and OAuth2 access tokens are not accepted, preventing automated revocation of OAuth2 applications - To disable an application temporarily without revoking tokens, use [Update OAuth2 client](/api-reference/oauth2/update-client) and set `isActive` to `false` ## Related Endpoints - [List OAuth2 clients](/api-reference/oauth2/list-clients) - [Get OAuth2 client](/api-reference/oauth2/get-client) - [Create OAuth2 client](/api-reference/oauth2/create-client) - [Update OAuth2 client](/api-reference/oauth2/update-client) - [Revoke token](/api-reference/oauth2/revoke-token) --- ## Revoke token > Revoke an active OAuth2 access token or refresh token per RFC 7009. URL: https://docs.storno.ro/api-reference/oauth2/revoke-token # Revoke token Revokes an active OAuth2 access token or refresh token. This is a public endpoint — no `Authorization` header is required. The client authenticates using `client_id` and `client_secret` in the request body. Per [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009), this endpoint always returns `200 OK` regardless of whether the token was found or already revoked. This prevents token enumeration attacks. ```http POST /api/v1/oauth2/revoke ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `token` | string | Yes | The access token (`storno_oat_...`) or refresh token (`storno_ort_...`) to revoke | | `token_type_hint` | string | No | A hint about the token type to speed up lookup. Either `access_token` or `refresh_token`. If omitted, Storno will attempt to detect the type automatically | | `client_id` | string | Yes | The client's public identifier, prefixed with `storno_cid_` | | `client_secret` | string | Confidential only | The client secret for `confidential` clients. Omit for `public` clients | ## Response Always returns `200 OK` with an empty body, per RFC 7009. ## Example Request — Revoking an access token ```bash curl -X POST 'https://api.storno.ro/api/v1/oauth2/revoke' \ -H 'Content-Type: application/json' \ -d '{ "token": "storno_oat_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "token_type_hint": "access_token", "client_id": "storno_cid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "client_secret": "storno_cs_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" }' ``` ## Example Request — Revoking a refresh token ```bash curl -X POST 'https://api.storno.ro/api/v1/oauth2/revoke' \ -H 'Content-Type: application/json' \ -d '{ "token": "storno_ort_z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4j3i2h1g0f9e8d7c6b5a4z3y2x1w0v9u8", "token_type_hint": "refresh_token", "client_id": "storno_cid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "client_secret": "storno_cs_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" }' ``` ## Example Response ```http HTTP/1.1 200 OK ``` ## Errors This endpoint returns `200 OK` even for unknown or already-revoked tokens, per RFC 7009. The only error condition that returns a non-200 response is invalid client authentication. | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | invalid_client | The `client_id` does not exist, or the `client_secret` is incorrect for a `confidential` client | ## Important Notes - **RFC 7009 compliance** — a `200 OK` response does not confirm that the token was valid or found. It only confirms that the token will not be accepted for API calls going forward, regardless of its prior state - **Revoking a refresh token** cascades to the access token that was issued alongside it. The associated access token is also invalidated immediately. The reverse is not true — revoking an access token does not invalidate the corresponding refresh token - **Logout flows** — when implementing user logout in your application, revoke both the access token and the refresh token to ensure clean session termination. Present the `token_type_hint` for each call to speed up server-side lookup - The `token_type_hint` is advisory only — if the hint does not match the actual token type, Storno will still find and revoke the correct token by searching both token stores ## Related Endpoints - [Token endpoint](/api-reference/oauth2/token) - [Authorization endpoint](/api-reference/oauth2/authorize) - [Revoke OAuth2 client](/api-reference/oauth2/revoke-client) - [OAuth2 Provider overview](/api-reference/oauth2/overview) --- ## Rotate client secret > Generate a new client secret for a confidential OAuth2 application, immediately invalidating the previous one. URL: https://docs.storno.ro/api-reference/oauth2/rotate-secret # Rotate client secret Generates a new `clientSecret` for a `confidential` OAuth2 application. The previous secret is immediately invalidated — any token exchange requests still using it will fail. The new raw secret value is returned **only once** in this response; store it securely immediately. This endpoint is only available for `confidential` clients. `public` clients do not have a client secret. This endpoint requires an active browser session (JWT). It cannot be called using an API token or an OAuth2 access token. ```http POST /api/v1/oauth2/clients/{uuid}/rotate-secret ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the OAuth2 application whose secret should be rotated | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer JWT from an active browser session | ### Required permission `oauth2_app.manage` ## Response Returns the updated OAuth2Client object with a `200 OK` status. The response includes a `clientSecret` field containing the new raw secret value. This field is **not** included in any subsequent response. ### Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier (UUID) | | `name` | string | Display name of the application | | `description` | string \| null | Application description | | `clientId` | string | Public client identifier (unchanged) | | `clientSecret` | string | The full raw new client secret — store this securely, it will not be shown again | | `clientSecretPrefix` | string | Updated prefix of the new client secret | | `clientType` | string | Always `confidential` for this operation | | `redirectUris` | string[] | Registered redirect URIs (unchanged) | | `scopes` | string[] | Permitted scopes (unchanged) | | `websiteUrl` | string \| null | Application website URL (unchanged) | | `logoUrl` | string \| null | Application logo URL (unchanged) | | `isActive` | boolean | Active status (unchanged) | | `revokedAt` | string \| null | Revocation timestamp (unchanged) | | `createdAt` | string | ISO 8601 creation timestamp (unchanged) | ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/oauth2/clients/a1b2c3d4-e5f6-7890-abcd-ef1234567890/rotate-secret' \ -H 'Authorization: Bearer YOUR_JWT_TOKEN' ``` ## Example Response ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Acme Accounting Integration", "description": "Syncs invoices from Storno to Acme Accounting in real time.", "clientId": "storno_cid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "clientSecret": "storno_cs_z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4j3i2h1g0f9e8d7c6b5a4z3y2x1w0v9u8", "clientSecretPrefix": "storno_cs_z9y8", "clientType": "confidential", "redirectUris": ["https://acme-accounting.com/oauth/callback"], "scopes": ["invoice.view", "client.view"], "websiteUrl": "https://acme-accounting.com", "logoUrl": "https://acme-accounting.com/logo.png", "isActive": true, "revokedAt": null, "createdAt": "2026-01-15T09:00:00Z" } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token, or token is not a browser session JWT | | 403 | forbidden | The authenticated user does not have the `oauth2_app.manage` permission, or the application is a `public` client | | 404 | not_found | OAuth2 application not found, or belongs to a different organization | ## Important Notes - The old `clientSecret` stops working the moment this call returns — update your application's configuration before rotating if you need zero-downtime rollover - The new `clientSecret` in the response is the only time the raw value is ever transmitted — it is stored as a one-way hash server-side - Rotating the secret does **not** revoke existing access tokens or refresh tokens; those remain valid until they expire or are individually revoked - Existing refresh tokens will stop working when they next attempt to use the old secret for client authentication during a token exchange - This operation cannot be performed on `public` clients, which have no client secret - This endpoint requires a browser session JWT. API tokens and OAuth2 access tokens are not accepted ## Related Endpoints - [List OAuth2 clients](/api-reference/oauth2/list-clients) - [Get OAuth2 client](/api-reference/oauth2/get-client) - [Create OAuth2 client](/api-reference/oauth2/create-client) - [Update OAuth2 client](/api-reference/oauth2/update-client) - [Revoke OAuth2 client](/api-reference/oauth2/revoke-client) --- ## Token endpoint > Exchange an authorization code or refresh token for a new set of OAuth2 tokens. URL: https://docs.storno.ro/api-reference/oauth2/token # Token endpoint Exchanges credentials for access and refresh tokens. This is a public endpoint — no `Authorization` header is required. Client authentication is performed via `client_id` and `client_secret` in the request body (for confidential clients). Two grant types are supported: `authorization_code` for the initial token exchange after user authorization, and `refresh_token` for obtaining a new token pair after the access token expires. ```http POST /api/v1/oauth2/token ``` This endpoint is rate-limited to **20 requests per minute** per client in production. --- ## Grant type: authorization_code Exchanges a single-use authorization code (obtained from the [Authorization endpoint](/api-reference/oauth2/authorize)) for an access token and refresh token. ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `grant_type` | string | Yes | Must be `authorization_code` | | `code` | string | Yes | The authorization code received in the redirect from the authorization endpoint | | `redirect_uri` | string | Yes | Must exactly match the `redirect_uri` used in the authorization request | | `client_id` | string | Yes | The client's public identifier, prefixed with `storno_cid_` | | `client_secret` | string | Confidential only | The client secret for `confidential` clients. Omit for `public` clients | | `code_verifier` | string | Yes | The original PKCE code verifier. Storno will verify `SHA-256(code_verifier)` matches the `code_challenge` from the authorization request | ### Response Returns `200 OK` with the token set. | Field | Type | Description | |-------|------|-------------| | `access_token` | string | Short-lived access token, prefixed with `storno_oat_`. Valid for 1 hour | | `refresh_token` | string | Long-lived refresh token, prefixed with `storno_ort_`. Valid for 30 days | | `token_type` | string | Always `Bearer` | | `expires_in` | number | Lifetime of the access token in seconds. Always `3600` | | `scope` | string | Space-separated list of scopes granted by the user | ### Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/oauth2/token' \ -H 'Content-Type: application/json' \ -d '{ "grant_type": "authorization_code", "code": "4/0AfJohXlQs8kKpM7nNrZhQ2vWxyz", "redirect_uri": "https://acme-accounting.com/oauth/callback", "client_id": "storno_cid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "client_secret": "storno_cs_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" }' ``` ### Example Response ```json { "access_token": "storno_oat_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "refresh_token": "storno_ort_z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4j3i2h1g0f9e8d7c6b5a4z3y2x1w0v9u8", "token_type": "Bearer", "expires_in": 3600, "scope": "invoice.view client.view" } ``` ### Validation The following checks are performed before issuing tokens: - The authorization code has not expired (codes are valid for 10 minutes) - The authorization code has not already been used (codes are single-use) - The `redirect_uri` matches the URI used when the code was issued - The `client_id` matches the client that requested the code - The `client_secret` is valid (confidential clients only) - The `code_verifier` produces the correct `code_challenge` via `SHA-256` --- ## Grant type: refresh_token Exchanges a valid refresh token for a new access token and a new refresh token. Refresh tokens are rotated on every use — the old refresh token is invalidated immediately. ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `grant_type` | string | Yes | Must be `refresh_token` | | `refresh_token` | string | Yes | A valid, unused refresh token prefixed with `storno_ort_` | | `client_id` | string | Yes | The client's public identifier, prefixed with `storno_cid_` | | `client_secret` | string | Confidential only | The client secret for `confidential` clients. Omit for `public` clients | ### Response Returns `200 OK` with a new token set. The structure is identical to the `authorization_code` response. | Field | Type | Description | |-------|------|-------------| | `access_token` | string | New access token, prefixed with `storno_oat_`. Valid for 1 hour | | `refresh_token` | string | New refresh token, prefixed with `storno_ort_`. Valid for 30 days. The old refresh token is now invalid | | `token_type` | string | Always `Bearer` | | `expires_in` | number | Lifetime of the new access token in seconds. Always `3600` | | `scope` | string | Space-separated list of granted scopes (unchanged from original authorization) | ### Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/oauth2/token' \ -H 'Content-Type: application/json' \ -d '{ "grant_type": "refresh_token", "refresh_token": "storno_ort_z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4j3i2h1g0f9e8d7c6b5a4z3y2x1w0v9u8", "client_id": "storno_cid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "client_secret": "storno_cs_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" }' ``` ### Example Response ```json { "access_token": "storno_oat_c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4", "refresh_token": "storno_ort_w6v5u4t3s2r1q0p9o8n7m6l5k4j3i2h1g0f9e8d7c6b5a4z3y2x1w0v9u8t7s6r5", "token_type": "Bearer", "expires_in": 3600, "scope": "invoice.view client.view" } ``` --- ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | invalid_request | Missing or malformed request parameters | | 400 | invalid_grant | The authorization code or refresh token is invalid, expired, already used, or does not belong to the provided `client_id` | | 400 | invalid_client | The `client_id` does not exist or the `client_secret` is incorrect | | 400 | unsupported_grant_type | The `grant_type` is not `authorization_code` or `refresh_token` | | 400 | invalid_scope | Requested scopes are not a subset of the authorized scopes | | 429 | rate_limited | Too many requests — the client has exceeded 20 requests per minute | ## Security Notes - **Refresh token rotation** — every successful refresh produces a new refresh token and immediately invalidates the old one. Do not attempt to use the same refresh token twice - **Token family revocation** — if a previously revoked refresh token is presented to this endpoint, Storno treats this as a replay attack and immediately revokes the **entire token family**, invalidating all active access and refresh tokens for that authorization. The associated client application should prompt the user to re-authorize - **PKCE verification** — for `authorization_code` grants, `code_verifier` is mandatory and verified using S256. Requests without a valid `code_verifier` are rejected even if the code itself is valid - Authorization codes are **single-use**. Presenting a code a second time returns `invalid_grant` and may trigger additional security measures ## Related Endpoints - [Authorization endpoint](/api-reference/oauth2/authorize) - [Revoke token](/api-reference/oauth2/revoke-token) - [OAuth2 Provider overview](/api-reference/oauth2/overview) --- ## Update OAuth2 client > Update an OAuth2 application's configuration, including its redirect URIs, scopes, and display metadata. URL: https://docs.storno.ro/api-reference/oauth2/update-client # Update OAuth2 client Updates an existing OAuth2 application. This is a partial update — only the fields included in the request body are changed. Fields omitted from the body are left unchanged. This endpoint requires an active browser session (JWT). It cannot be called using an API token or an OAuth2 access token. ```http PATCH /api/v1/oauth2/clients/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the OAuth2 application to update | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer JWT from an active browser session | | `Content-Type` | string | Yes | Must be `application/json` | ### Required permission `oauth2_app.manage` ### Body Parameters All parameters are optional, but at least one must be provided. | Parameter | Type | Description | |-----------|------|-------------| | `name` | string | New human-readable display name for the application | | `description` | string \| null | Updated description of the application's purpose. Pass `null` to clear | | `redirectUris` | string[] | Replacement set of allowed redirect URIs. Replaces the entire existing list | | `scopes` | string[] | Replacement set of permitted scopes. Must be valid `Permission` values and a subset of the authenticated user's own permissions. Replaces the entire existing list | | `isActive` | boolean | Set to `false` to disable the application without revoking it. Disabled applications cannot initiate new authorization flows but existing tokens remain valid | | `websiteUrl` | string \| null | Updated application website URL. Pass `null` to clear | | `logoUrl` | string \| null | Updated application logo URL. Pass `null` to clear | ## Response Returns the updated OAuth2Client object with a `200 OK` status. The `clientSecret` is never included in this response. ### Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier (UUID) | | `name` | string | Updated display name | | `description` | string \| null | Updated description | | `clientId` | string | Public client identifier (unchanged) | | `clientSecretPrefix` | string \| null | First characters of the client secret (unchanged) | | `clientType` | string | `confidential` or `public` (cannot be changed after creation) | | `redirectUris` | string[] | Updated redirect URIs | | `scopes` | string[] | Updated permitted scopes | | `websiteUrl` | string \| null | Updated website URL | | `logoUrl` | string \| null | Updated logo URL | | `isActive` | boolean | Updated active status | | `revokedAt` | string \| null | ISO 8601 revocation timestamp, or `null` if still active | | `createdAt` | string | ISO 8601 creation timestamp (unchanged) | ## Example Request ```bash curl -X PATCH 'https://api.storno.ro/api/v1/oauth2/clients/a1b2c3d4-e5f6-7890-abcd-ef1234567890' \ -H 'Authorization: Bearer YOUR_JWT_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "Acme Accounting Integration v2", "redirectUris": [ "https://acme-accounting.com/oauth/callback", "https://acme-accounting.com/oauth/callback-v2" ], "scopes": ["invoice.view", "invoice.create", "client.view"] }' ``` ## Example Response ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Acme Accounting Integration v2", "description": "Syncs invoices from Storno to Acme Accounting in real time.", "clientId": "storno_cid_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", "clientSecretPrefix": "storno_cs_a1b2", "clientType": "confidential", "redirectUris": [ "https://acme-accounting.com/oauth/callback", "https://acme-accounting.com/oauth/callback-v2" ], "scopes": ["invoice.view", "invoice.create", "client.view"], "websiteUrl": "https://acme-accounting.com", "logoUrl": "https://acme-accounting.com/logo.png", "isActive": true, "revokedAt": null, "createdAt": "2026-01-15T09:00:00Z" } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token, or token is not a browser session JWT | | 403 | forbidden | The authenticated user does not have the `oauth2_app.manage` permission | | 404 | not_found | OAuth2 application not found, or belongs to a different organization | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - No fields provided for update - One or more `redirectUris` are not valid URLs - One or more scope values are not valid `Permission` values - One or more scopes exceed the authenticated user's own permissions - `name` is an empty string ## Important Notes - Updating `redirectUris` or `scopes` replaces the full list — it is not an additive operation. Send the complete desired set - The `clientType` cannot be changed after creation. Revoke and recreate the client if a different type is needed - Setting `isActive` to `false` prevents new authorization flows but does **not** invalidate existing tokens. To immediately revoke all tokens, use [Revoke OAuth2 client](/api-reference/oauth2/revoke-client) - Narrowing the `scopes` list does not immediately revoke tokens that were issued with broader scopes; those tokens will continue to work until they expire or are individually revoked - This endpoint requires a browser session JWT. API tokens and OAuth2 access tokens are not accepted ## Related Endpoints - [List OAuth2 clients](/api-reference/oauth2/list-clients) - [Get OAuth2 client](/api-reference/oauth2/get-client) - [Create OAuth2 client](/api-reference/oauth2/create-client) - [Revoke OAuth2 client](/api-reference/oauth2/revoke-client) - [Rotate client secret](/api-reference/oauth2/rotate-secret) --- ## Delete invoice payment > Delete a recorded payment from an invoice. URL: https://docs.storno.ro/api-reference/payments/delete # Delete invoice payment Deletes a recorded payment from an invoice. This updates the invoice's `amountPaid` field and may change the invoice status. ```http DELETE /api/v1/invoices/{uuid}/payments/{paymentId} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the invoice | | `paymentId` | string | Yes | The UUID of the payment to delete | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns a `204 No Content` status on successful deletion with no response body. ## Example Request ```bash curl -X DELETE 'https://api.storno.ro/api/v1/invoices/invoice-uuid-1/payments/payment-uuid-2' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```http HTTP/1.1 204 No Content ``` ## Behavior Deleting a payment: 1. Removes the payment record permanently 2. Decreases the invoice's `amountPaid` by the payment amount 3. Recalculates the invoice status based on remaining payments: - If `amountPaid` becomes 0, status changes to `unpaid` - If 0 < `amountPaid` < `totalAmount`, status becomes `partially_paid` - If `amountPaid` ≥ `totalAmount`, status remains `paid` ## Use Cases ### Correcting Errors Delete an incorrectly recorded payment and create a new one with correct details. ### Reversing Payments Remove a payment that was later reversed or refunded. ### Data Cleanup Clean up duplicate or test payments. ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Invoice or payment not found, or doesn't belong to company | ## Important Notes - This is a permanent delete operation - data cannot be recovered - The invoice's `amountPaid` is automatically recalculated - The invoice status is automatically updated based on remaining payments - Consider the accounting implications before deleting payments - For audit purposes, you may want to record the deletion reason externally ## Recommended Approach Instead of deleting: 1. **Add a correction payment** - For accounting accuracy, consider adding a negative payment or correction entry 2. **Document the reason** - Keep external notes explaining why payments were deleted 3. **Verify impact** - Check the updated invoice status after deletion ## Related Endpoints - [List payments](/api-reference/payments/list) - [Record payment](/api-reference/payments/create) - [Get invoice](/api-reference/invoices/get) --- ## List invoice payments > Retrieve all payments recorded for a specific invoice. URL: https://docs.storno.ro/api-reference/payments/list # List invoice payments Retrieves all payments recorded for a specific invoice, ordered by payment date (most recent first). ```http GET /api/v1/invoices/{uuid}/payments ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the invoice | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns an array of payment objects. ### Payment Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `amount` | number | Payment amount | | `currency` | string | Currency code (ISO 4217) | | `paymentDate` | string | ISO 8601 payment date | | `paymentMethod` | string | Payment method: `bank_transfer`, `cash`, `card`, `other` | | `reference` | string \| null | Payment reference number | | `notes` | string \| null | Additional notes | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/invoices/invoice-uuid-1/payments' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json [ { "uuid": "payment-uuid-1", "amount": 1500.00, "currency": "RON", "paymentDate": "2026-02-15", "paymentMethod": "bank_transfer", "reference": "TRF-2026-02-15-001", "notes": "Virament bancar în cont principal", "createdAt": "2026-02-15T14:30:00Z", "updatedAt": "2026-02-15T14:30:00Z" }, { "uuid": "payment-uuid-2", "amount": 880.00, "currency": "RON", "paymentDate": "2026-02-10", "paymentMethod": "bank_transfer", "reference": "TRF-2026-02-10-045", "notes": "Plată parțială", "createdAt": "2026-02-10T10:15:00Z", "updatedAt": "2026-02-10T10:15:00Z" } ] ``` ## Payment Methods | Value | Description | |-------|-------------| | `bank_transfer` | Bank transfer / wire transfer | | `cash` | Cash payment | | `card` | Card payment (credit/debit) | | `other` | Other payment method | ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Invoice not found or doesn't belong to company | ## Important Notes - Payments are ordered by `paymentDate` in descending order (most recent first) - The sum of all payment amounts should not exceed the invoice total - Each payment updates the invoice's `amountPaid` field - When `amountPaid` equals `totalAmount`, the invoice status becomes `paid` - When `amountPaid` is greater than 0 but less than `totalAmount`, status becomes `partially_paid` ## Related Endpoints - [Record payment](/api-reference/payments/create) - [Delete payment](/api-reference/payments/delete) - [Get invoice](/api-reference/invoices/get) --- ## Record invoice payment > Record a payment received for a specific invoice. URL: https://docs.storno.ro/api-reference/payments/create # Record invoice payment Records a payment received for a specific invoice. This updates the invoice's `amountPaid` field and may change the invoice status to `paid` or `partially_paid`. ```http POST /api/v1/invoices/{uuid}/payments ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the invoice | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `amount` | number | Yes | Payment amount (must be > 0) | | `paymentDate` | string | Yes | ISO 8601 date of payment | | `paymentMethod` | string | No | Payment method (default: `bank_transfer`) | | `currency` | string | No | Currency code (defaults to invoice currency) | | `reference` | string | No | Payment reference number | | `notes` | string | No | Additional notes about the payment | ## Response Returns the created payment object with a `201 Created` status and the updated invoice summary. ### Response Schema | Field | Type | Description | |-------|------|-------------| | `payment` | object | The created payment object | | `invoice` | object | Updated invoice summary | ### Invoice Summary | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Invoice UUID | | `number` | string | Invoice number | | `totalAmount` | number | Total invoice amount | | `amountPaid` | number | Updated amount paid | | `remainingAmount` | number | Remaining unpaid amount | | `status` | string | Updated invoice status | ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/invoices/invoice-uuid-1/payments' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "amount": 1500.00, "paymentDate": "2026-02-15", "paymentMethod": "bank_transfer", "currency": "RON", "reference": "TRF-2026-02-15-001", "notes": "Virament bancar în cont principal" }' ``` ## Example Response ```json { "payment": { "uuid": "payment-uuid-1", "amount": 1500.00, "currency": "RON", "paymentDate": "2026-02-15", "paymentMethod": "bank_transfer", "reference": "TRF-2026-02-15-001", "notes": "Virament bancar în cont principal", "createdAt": "2026-02-15T14:30:00Z", "updatedAt": "2026-02-15T14:30:00Z" }, "invoice": { "uuid": "invoice-uuid-1", "number": "FAC00245", "totalAmount": 2380.00, "amountPaid": 1500.00, "remainingAmount": 880.00, "status": "partially_paid" } } ``` ## Payment Methods | Value | Description | Common Use Case | |-------|-------------|-----------------| | `bank_transfer` | Bank transfer | Default method, most common | | `cash` | Cash payment | In-person transactions | | `card` | Card payment | Credit/debit card transactions | | `other` | Other method | Alternative payment systems | ## Invoice Status Updates Recording a payment automatically updates the invoice status: | Condition | Status | |-----------|--------| | `amountPaid` = 0 | `unpaid` | | 0 < `amountPaid` < `totalAmount` | `partially_paid` | | `amountPaid` ≥ `totalAmount` | `paid` | ## Validation Rules - Payment amount must be greater than 0 - Payment date cannot be in the future - Currency should match invoice currency (warning if different) - Total payments cannot exceed invoice total by more than a small tolerance (e.g., 0.01) - Payment date should not be before invoice issue date (warning only) ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Invoice not found or doesn't belong to company | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - Missing `amount` field - Amount is zero or negative - Missing `paymentDate` field - Invalid date format - Payment date is in the future - Total payments exceed invoice total - Currency mismatch with invoice ## Important Notes - Recording a payment updates the invoice's `amountPaid` in real-time - Overpayments are allowed with a tolerance but will generate a warning - For partial payments, record each payment separately - Payments are immutable once created; use delete to correct errors - Payment data is preserved even if the invoice is later modified ## Related Endpoints - [List payments](/api-reference/payments/list) - [Delete payment](/api-reference/payments/delete) - [Get invoice](/api-reference/invoices/get) --- ## Get PDF Template Configuration > Retrieve the PDF template configuration for a company URL: https://docs.storno.ro/api-reference/pdf-template-config/get # Get PDF Template Configuration Returns the current PDF template configuration for the company. If no configuration exists, returns a default configuration with the `classic` template. ``` GET /api/v1/pdf-template-config ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token or API key for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Request ```bash {% title="cURL" %} curl https://api.storno.ro/api/v1/pdf-template-config \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ## Response ```json { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "templateSlug": "classic", "primaryColor": "#2563eb", "fontFamily": "DejaVu Sans", "showLogo": true, "showBankInfo": true, "footerText": null, "customCss": null } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Configuration UUID | | `templateSlug` | string | Active template design (`classic`, `modern`, `minimal`, `bold`) | | `primaryColor` | string\|null | Primary brand color in hex format | | `fontFamily` | string\|null | CSS font family used in PDFs | | `showLogo` | boolean | Whether the company logo is displayed | | `showBankInfo` | boolean | Whether bank account info is displayed | | `footerText` | string\|null | Custom footer text | | `customCss` | string\|null | Custom CSS injected into the template | ## Error Codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `404` | Company not found | --- ## List Available Templates > List all available PDF template designs URL: https://docs.storno.ro/api-reference/pdf-template-config/list-templates # List Available Templates Returns all available PDF template designs with their metadata. Use the `slug` value when updating the PDF template configuration. ``` GET /api/v1/pdf-template-config/templates ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token or API key for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Request ```bash {% title="cURL" %} curl https://api.storno.ro/api/v1/pdf-template-config/templates \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ## Response ```json [ { "slug": "classic", "name": "Clasic", "description": "Design traditional cu linii curate si culori profesionale", "defaultColor": "#2563eb" }, { "slug": "modern", "name": "Modern", "description": "Design modern cu colturi rotunjite si antet colorat", "defaultColor": "#6366f1" }, { "slug": "minimal", "name": "Minimal", "description": "Design minimalist cu linii fine si aspect compact", "defaultColor": "#374151" }, { "slug": "bold", "name": "Indrăzneț", "description": "Design puternic cu bara de culoare si totaluri mari", "defaultColor": "#dc2626" } ] ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `slug` | string | Template identifier used in configuration | | `name` | string | Human-readable template name | | `description` | string | Brief description of the template style | | `defaultColor` | string | Default primary color for this template | ## Error Codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | --- ## Preview PDF Template > Generate an HTML preview of a PDF template with sample data URL: https://docs.storno.ro/api-reference/pdf-template-config/preview # Preview PDF Template Generates an HTML preview of a PDF template using sample invoice data. Use this to preview how a template will look with specific colors and fonts before saving the configuration. ``` POST /api/v1/pdf-template-config/preview ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token or API key for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `templateSlug` | string | No | Template to preview (default: `classic`) | | `primaryColor` | string | No | Primary color to preview in hex format | | `fontFamily` | string | No | Font family to preview | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/pdf-template-config/preview \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{ "templateSlug": "modern", "primaryColor": "#6366f1" }' ``` ## Response Returns the rendered HTML preview. **Headers:** | Header | Value | |--------|-------| | `Content-Type` | `text/html; charset=UTF-8` | The response body contains the full HTML document that can be rendered in a browser or converted to PDF. ## Error Codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `404` | Company not found | | `500` | Preview generation failed | --- ## Update PDF Template Configuration > Update the PDF template configuration for a company URL: https://docs.storno.ro/api-reference/pdf-template-config/update # Update PDF Template Configuration Updates the PDF template configuration for the company. All fields are optional - only include the fields you want to change. ``` PUT /api/v1/pdf-template-config ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token or API key for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `templateSlug` | string | No | Template design: `classic`, `modern`, `minimal`, or `bold` | | `primaryColor` | string\|null | No | Primary color in hex format (`#RRGGBB`). Set to `null` to use template default. | | `fontFamily` | string\|null | No | CSS font family (e.g., `"DejaVu Sans"`, `"Roboto"`) | | `showLogo` | boolean | No | Display company logo on PDFs (default: `true`) | | `showBankInfo` | boolean | No | Display bank account info on PDFs (default: `true`) | | `footerText` | string\|null | No | Custom footer text. Set to `null` to remove. | | `customCss` | string\|null | No | Custom CSS styles. Set to `null` to remove. | ## Request ```bash {% title="cURL" %} curl -X PUT https://api.storno.ro/api/v1/pdf-template-config \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{ "templateSlug": "modern", "primaryColor": "#6366f1", "showLogo": true, "showBankInfo": true, "footerText": "Thank you for your business!" }' ``` ## Response Returns the updated configuration object. ```json { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "templateSlug": "modern", "primaryColor": "#6366f1", "fontFamily": "DejaVu Sans", "showLogo": true, "showBankInfo": true, "footerText": "Thank you for your business!", "customCss": null } ``` ## Validation - `templateSlug` must be one of: `classic`, `modern`, `minimal`, `bold` - `primaryColor` must match hex format `#[0-9a-fA-F]{6}` or be `null` ## Error Codes | Code | Description | |------|-------------| | `400` | Invalid template slug or color format | | `401` | Missing or invalid authentication token | | `404` | Company not found | --- ## Create product category > Create a new POS product category for the active company. URL: https://docs.storno.ro/api-reference/product-categories/create # Create product category ```http POST /api/v1/product-categories ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token | | `X-Company` | string | Yes | Company UUID | | `Content-Type` | string | Yes | `application/json` | ## Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | 1–100 characters, displayed on the POS chip | | `color` | string | No | Hex swatch — `"#RRGGBB"` or `"RRGGBB"`. Invalid values are silently dropped. | | `sortOrder` | integer | No | Default 0. Smaller = earlier in the chip strip | ## Response `201 Created` — same shape as the list entry. ## Permissions Requires `product.edit`. --- ## Delete product category > Remove a product category. Products in the category are not deleted; their categoryId is set to null. URL: https://docs.storno.ro/api-reference/product-categories/delete # Delete product category ```http DELETE /api/v1/product-categories/{uuid} ``` The product → category relation uses `ON DELETE SET NULL`, so any products that were in this category lose the assignment but remain intact. ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Category UUID | ## Response `200 OK`. ```json { "message": "Category deleted." } ``` ## Permissions Requires `product.edit`. --- ## List product categories > List all product categories for the active company, ordered by sortOrder then name. URL: https://docs.storno.ro/api-reference/product-categories/list # List product categories Returns all product categories defined for the company. Categories appear as a horizontal chip strip above the POS product grid; tapping one filters the grid to that category. ```http GET /api/v1/product-categories ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token | | `X-Company` | string | Yes | Company UUID | ## Response ```json { "data": [ { "id": "0a..f", "name": "Cafele", "color": "#7c3aed", "sortOrder": 0 }, { "id": "1b..e", "name": "Sandwich-uri", "color": "#16a34a", "sortOrder": 1 } ] } ``` | Field | Type | Description | |-------|------|-------------| | `id` | string | Category UUID | | `name` | string | Display name (1–100 chars) | | `color` | string \| null | Optional hex swatch (`#RRGGBB`) used for the chip and as Product card fallback | | `sortOrder` | integer | Sort key (smaller = earlier) | ## Permissions Requires `product.view`. --- ## Update product category > Edit a product category's name, colour, or sort order. URL: https://docs.storno.ro/api-reference/product-categories/update # Update product category ```http PATCH /api/v1/product-categories/{uuid} ``` All fields are optional; omitted fields stay unchanged. ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Category UUID | ## Body | Field | Type | Description | |-------|------|-------------| | `name` | string | 1–100 characters; empty rejected with `400` | | `color` | string \| null | Pass null to clear the swatch | | `sortOrder` | integer | Reorder in the chip strip | ## Response `200 OK` with the updated category. ## Permissions Requires `product.edit`. --- ## Get product > Retrieve detailed information about a specific product. URL: https://docs.storno.ro/api-reference/products/get # Get product Retrieves detailed information about a specific product, including usage statistics. ```http GET /api/v1/products/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the product | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns a detailed product object. ### Response Schema | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `name` | string | Product name | | `code` | string \| null | Product code/SKU | | `description` | string \| null | Product description | | `unitPrice` | number | Default unit price | | `currency` | string | Default currency (ISO 4217) | | `vatRateId` | string | Default VAT rate UUID | | `vatRate` | number | Default VAT percentage | | `unitOfMeasure` | string | Default unit of measure | | `color` | string \| null | Optional hex colour swatch (e.g. `"#1e40af"`) shown on the POS product grid. When null, mobile clients fall back to a deterministic palette derived from the product UUID. | | `category` | object \| null | Optional [product category](/api-reference/product-categories/list) — `{ id, name, color, sortOrder }`. Used as fallback swatch and grid grouping on the POS. | | `sgrAmount` | string \| null | Romanian SGR (Sistem Garantie-Returnare) deposit per unit, e.g. `"0.50"` for plastic beverage bottles. Null when the product is not SGR-eligible. The deposit is VAT-exempt and appears as a separate auto-managed line on POS receipts. | | `isActive` | boolean | Whether product is active | | `usageStats` | object | Product usage statistics | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | ### Usage Stats Object | Field | Type | Description | |-------|------|-------------| | `totalUsage` | integer | Total times used in invoices | | `totalRevenue` | number | Total revenue generated | | `averageQuantity` | number | Average quantity per invoice | | `firstUsedDate` | string \| null | ISO 8601 date of first use | | `lastUsedDate` | string \| null | ISO 8601 date of last use | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/products/product-uuid-1' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json { "uuid": "product-uuid-1", "name": "Cloud Hosting - Business Plan", "code": "HOST-BIZ-001", "description": "Business cloud hosting package with 100GB storage and unlimited bandwidth", "unitPrice": 1499.00, "currency": "RON", "vatRateId": "vat-uuid-1", "vatRate": 19, "unitOfMeasure": "buc", "isActive": true, "usageStats": { "totalUsage": 48, "totalRevenue": 71952.00, "averageQuantity": 1.0, "firstUsedDate": "2025-06-15", "lastUsedDate": "2026-02-10" }, "createdAt": "2025-05-10T10:00:00Z", "updatedAt": "2026-01-15T14:30:00Z" } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Product not found or doesn't belong to company | ## Important Notes - Products in Storno.ro are sync-only and come from ANAF e-Factura system - Product data cannot be manually created or edited via API - Use the [ANAF sync endpoint](/api-reference/anaf/sync-invoices) to fetch latest product data - Products are automatically created from invoice line items during synchronization - Usage statistics are calculated in real-time based on invoice line items ## Related Endpoints - [List products](/api-reference/products/list) - [Sync from ANAF](/api-reference/anaf/sync-invoices) --- ## List products > Retrieve a paginated list of products with optional filtering. URL: https://docs.storno.ro/api-reference/products/list # List products Retrieves a paginated list of products for the authenticated company. Results can be filtered by active status and searched by name, code, or description. ```http GET /api/v1/products ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `page` | integer | No | Page number (default: 1) | | `limit` | integer | No | Items per page (default: 50, max: 200) | | `search` | string | No | Search term to filter by name, code, or description | | `isActive` | boolean | No | Filter by active status (true/false) | ## Response Returns a paginated list of product objects. ### Response Schema | Field | Type | Description | |-------|------|-------------| | `data` | array | Array of product objects | | `total` | integer | Total number of matching products | | `page` | integer | Current page number | | `limit` | integer | Items per page | | `pages` | integer | Total number of pages | ### Product Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `name` | string | Product name | | `code` | string \| null | Product code/SKU | | `description` | string \| null | Product description | | `unitPrice` | number | Default unit price | | `currency` | string | Default currency (ISO 4217) | | `vatRateId` | string | Default VAT rate UUID | | `vatRate` | number | Default VAT percentage | | `unitOfMeasure` | string | Default unit of measure | | `color` | string \| null | Optional hex colour swatch (e.g. `"#1e40af"`) shown on the POS product grid. | | `category` | object \| null | Optional [product category](/api-reference/product-categories/list) — `{ id, name, color, sortOrder }`. | | `sgrAmount` | string \| null | Romanian SGR deposit per unit (e.g. `"0.50"`). | | `isActive` | boolean | Whether product is active | | `usageCount` | integer | Number of times used in invoices | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/products?page=1&limit=50&isActive=true' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json { "data": [ { "uuid": "product-uuid-1", "name": "Cloud Hosting - Business Plan", "code": "HOST-BIZ-001", "description": "Business cloud hosting package with 100GB storage and unlimited bandwidth", "unitPrice": 1499.00, "currency": "RON", "vatRateId": "vat-uuid-1", "vatRate": 19, "unitOfMeasure": "buc", "isActive": true, "usageCount": 48, "createdAt": "2025-05-10T10:00:00Z", "updatedAt": "2026-01-15T14:30:00Z" }, { "uuid": "product-uuid-2", "name": "Cloud Hosting - Premium Plan", "code": "HOST-PREM-001", "description": "Premium cloud hosting package with 500GB storage and dedicated resources", "unitPrice": 2499.00, "currency": "RON", "vatRateId": "vat-uuid-1", "vatRate": 19, "unitOfMeasure": "buc", "isActive": true, "usageCount": 22, "createdAt": "2025-05-10T10:05:00Z", "updatedAt": "2026-02-01T09:20:00Z" }, { "uuid": "product-uuid-3", "name": "SSL Certificate - Single Domain", "code": "SSL-SINGLE-001", "description": "Standard SSL certificate for single domain, valid 1 year", "unitPrice": 299.00, "currency": "RON", "vatRateId": "vat-uuid-1", "vatRate": 19, "unitOfMeasure": "buc", "isActive": true, "usageCount": 15, "createdAt": "2025-06-20T11:30:00Z", "updatedAt": "2025-12-10T16:45:00Z" } ], "total": 32, "page": 1, "limit": 50, "pages": 1 } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 422 | validation_error | Invalid query parameters | ## Important Notes - Products in Storno.ro are sync-only and come from ANAF e-Factura system - Product data cannot be manually created or edited via API - Use the [ANAF sync endpoint](/api-reference/anaf/sync-invoices) to fetch latest product data - Products are extracted from invoice line items during synchronization - `usageCount` reflects how many times a product has been used in invoice lines ## Related Endpoints - [Get product](/api-reference/products/get) - [Sync from ANAF](/api-reference/anaf/sync-invoices) --- ## Accept Proforma Invoice > Mark a proforma invoice as accepted by the client URL: https://docs.storno.ro/api-reference/proforma-invoices/accept # Accept Proforma Invoice Marks a proforma invoice as accepted by the client. This action transitions the proforma to `accepted` status and records the acceptance timestamp. Once accepted, the proforma is ready to be converted into a final invoice. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the proforma invoice to mark as accepted | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000/accept \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000/accept', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response Returns the updated proforma invoice with `status = accepted` and `acceptedAt` timestamp: ```json { "uuid": "550e8400-e29b-41d4-a716-446655440000", "number": "PRO-2026-001", "seriesId": "650e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "650e8400-e29b-41d4-a716-446655440000", "name": "PRO", "nextNumber": 2 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "email": "contact@client.ro" }, "status": "accepted", "issueDate": "2026-02-16", "dueDate": "2026-03-16", "validUntil": "2026-03-16", "currency": "RON", "exchangeRate": 1.0, "subtotal": "7000.00", "vatAmount": "1330.00", "total": "8330.00", "notes": "Payment terms: 30 days", "paymentTerms": "Net 30", "sentAt": "2026-02-16T10:30:00Z", "acceptedAt": "2026-02-17T09:15:00Z", "rejectedAt": null, "cancelledAt": null, "convertedAt": null, "convertedInvoiceId": null, "createdAt": "2026-02-16T09:00:00Z", "updatedAt": "2026-02-17T09:15:00Z" } ``` ## State Changes ### Status Transition - **Before:** `status = sent` (or `draft`) - **After:** `status = accepted` ### Timestamp - Sets `acceptedAt` to current UTC timestamp (ISO 8601 format) - Updates `updatedAt` timestamp ### Next Actions Once accepted, you can: - [Convert to invoice](/api-reference/proforma-invoices/convert) - Create a final invoice from the proforma - Keep as accepted - If client wants to delay invoicing - [Cancel](/api-reference/proforma-invoices/cancel) - If deal falls through ## Validation Rules ### Status Requirement - Proforma must have `status = sent` or `status = draft` - Cannot accept a proforma that is already accepted, rejected, converted, or cancelled ### Business Logic Accepting a proforma indicates: - Client agrees with the terms and pricing - Client commits to the purchase - Proforma is ready for conversion to invoice - No further changes to pricing or terms expected ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Proforma invoice not found or doesn't belong to the company | | 409 | `conflict` | Proforma status prevents acceptance | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict - Already Accepted ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be accepted", "details": { "status": "accepted", "reason": "Proforma invoice is already accepted", "acceptedAt": "2026-02-17T09:15:00Z" } } } ``` ### Status Conflict - Already Converted ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be accepted", "details": { "status": "converted", "reason": "Proforma invoice has already been converted to an invoice", "convertedAt": "2026-02-17T10:00:00Z", "convertedInvoiceId": "650e8400-e29b-41d4-a716-446655440111" } } } ``` ### Status Conflict - Cancelled ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be accepted", "details": { "status": "cancelled", "reason": "Proforma invoice has been cancelled", "cancelledAt": "2026-02-16T15:30:00Z" } } } ``` ## Workflow Integration ### Typical Flow 1. Create proforma (`POST /api/v1/proforma-invoices`) 2. Send to client (`POST /api/v1/proforma-invoices/{uuid}/send`) 3. Client reviews and approves 4. **Mark as accepted** (`POST /api/v1/proforma-invoices/{uuid}/accept`) ← You are here 5. Convert to invoice (`POST /api/v1/proforma-invoices/{uuid}/convert`) 6. Send invoice to client and ANAF ### Alternative Flow If client doesn't explicitly accept: - Skip the accept step - Convert directly from `sent` to `converted` - This is valid for trusted clients or standard orders ### Client Portal Integration If you have a client portal: - Allow clients to accept proformas themselves - Trigger this API call when they click "Accept" - Send confirmation email after acceptance - Notify sales team of acceptance ## Best Practices 1. **Record acceptance method** - Log how acceptance was received (email, phone, portal) 2. **Notify stakeholders** - Alert sales and accounting teams 3. **Trigger next steps** - Automatically initiate invoice conversion workflow 4. **Archive confirmation** - Store client's acceptance email/message 5. **Set conversion deadline** - Convert to invoice within reasonable timeframe 6. **Update CRM** - Sync acceptance status to your CRM system ## Acceptance vs Conversion **Accept** when: - Client explicitly approves the proforma - You need to track approval as a separate business event - There may be a delay between approval and invoicing - You want clear audit trail of client consent **Convert directly** when: - Client approval is implicit (repeat orders, standing agreements) - You want to invoice immediately after sending proforma - Acceptance tracking is not required for your workflow --- ## Cancel Proforma Invoice > Cancel a proforma invoice URL: https://docs.storno.ro/api-reference/proforma-invoices/cancel # Cancel Proforma Invoice Cancels a proforma invoice by transitioning it to `cancelled` status. This action is used when you need to withdraw or invalidate a proforma without deleting it from the system. Unlike deletion, cancellation preserves the proforma for historical records and audit trail while preventing any further actions. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the proforma invoice to cancel | ## Request Body (Optional) | Field | Type | Required | Description | |-------|------|----------|-------------| | `cancellationReason` | string | No | Reason for cancellation | | `cancellationNotes` | string | No | Internal notes about the cancellation | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000/cancel \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "cancellationReason": "Client changed requirements", "cancellationNotes": "New proforma to be created with updated specs" }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000/cancel', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ cancellationReason: 'Client changed requirements', cancellationNotes: 'New proforma to be created with updated specs' }) }); const data = await response.json(); ``` ## Response Returns the updated proforma invoice with `status = cancelled` and `cancelledAt` timestamp: ```json { "uuid": "550e8400-e29b-41d4-a716-446655440000", "number": "PRO-2026-001", "seriesId": "650e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "650e8400-e29b-41d4-a716-446655440000", "name": "PRO", "nextNumber": 2 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "email": "contact@client.ro" }, "status": "cancelled", "issueDate": "2026-02-16", "dueDate": "2026-03-16", "validUntil": "2026-03-16", "currency": "RON", "exchangeRate": 1.0, "subtotal": "7000.00", "vatAmount": "1330.00", "total": "8330.00", "notes": "Payment terms: 30 days", "paymentTerms": "Net 30", "cancellationReason": "Client changed requirements", "cancellationNotes": "New proforma to be created with updated specs", "sentAt": "2026-02-16T10:30:00Z", "acceptedAt": null, "rejectedAt": null, "cancelledAt": "2026-02-18T14:45:00Z", "convertedAt": null, "convertedInvoiceId": null, "createdAt": "2026-02-16T09:00:00Z", "updatedAt": "2026-02-18T14:45:00Z" } ``` ## State Changes ### Status Transition - **Before:** Any status except `converted` or `cancelled` - **After:** `status = cancelled` ### Timestamp - Sets `cancelledAt` to current UTC timestamp (ISO 8601 format) - Updates `updatedAt` timestamp ### Restrictions Applied Once cancelled: - Cannot be converted to invoice - Cannot be accepted or rejected - Cannot be edited or deleted - Cannot be sent (if was in draft) - Serves as historical record only ## Validation Rules ### Status Requirement Can cancel proforma in these statuses: - `draft` - Not yet sent - `sent` - Sent but not yet responded to - `accepted` - Accepted but not yet converted - `rejected` - Previously rejected Cannot cancel proforma in these statuses: - `converted` - Already converted to invoice (use credit note instead) - `cancelled` - Already cancelled ### Business Logic Cancelling a proforma indicates: - Offer is no longer valid - Terms have changed significantly - Project is cancelled or postponed - Error in proforma that cannot be corrected - Duplicate proforma created by mistake ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Proforma invoice not found or doesn't belong to the company | | 409 | `conflict` | Proforma status prevents cancellation (already converted or cancelled) | | 422 | `validation_error` | Invalid request body (if cancellation reason/notes provided) | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict - Already Cancelled ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be cancelled", "details": { "status": "cancelled", "reason": "Proforma invoice is already cancelled", "cancelledAt": "2026-02-18T14:45:00Z" } } } ``` ### Status Conflict - Already Converted ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be cancelled", "details": { "status": "converted", "reason": "Proforma invoice has already been converted to an invoice. Use credit note to reverse the invoice instead.", "convertedAt": "2026-02-17T10:00:00Z", "convertedInvoiceId": "650e8400-e29b-41d4-a716-446655440111" } } } ``` ## Cancellation Reasons ### Common Reasons - **Client request** - Client asked to cancel - **Requirements changed** - Scope or specifications changed - **Pricing error** - Wrong prices or calculations - **Duplicate** - Created by mistake - **Project postponed** - Client delays project - **Project cancelled** - Client cancels entirely - **Wrong client** - Sent to wrong client - **Expired** - Validity period passed without response - **Terms unacceptable** - Couldn't agree on terms - **Budget constraints** - Client has no budget ## Cancel vs Delete vs Reject ### Cancel - **Use when:** You want to withdraw the offer from your side - **Preserves:** Full audit trail and historical data - **Status:** `cancelled` - **Can be done:** At any status (except converted/cancelled) ### Delete - **Use when:** Proforma created by mistake (draft only) - **Preserves:** Nothing - permanently removed - **Status:** N/A - record is deleted - **Can be done:** Only in `draft` status ### Reject - **Use when:** Client declines the offer - **Preserves:** Full audit trail with client feedback - **Status:** `rejected` - **Can be done:** When client provides response ## Workflow Integration ### Cancellation Flow 1. Determine cancellation is necessary 2. **Call cancel endpoint** (`POST /api/v1/proforma-invoices/{uuid}/cancel`) ← You are here 3. Notify relevant stakeholders 4. Create replacement proforma if needed 5. Update CRM and project management systems ### Notification Strategy After cancellation: - Notify sales team - Update project management system - Log reason in CRM - Inform client if already sent - Archive related documents ## Best Practices 1. **Always provide cancellation reason** - Essential for analytics and audit 2. **Communicate with client** - Inform client if proforma was already sent 3. **Create replacement** - Issue new proforma with correct information if needed 4. **Track patterns** - Monitor cancellation reasons to improve processes 5. **Update linked systems** - Sync status to CRM and project tools 6. **Preserve relationships** - Handle cancellations professionally 7. **Document context** - Use internal notes for detailed context ## Analytics and Reporting ### Cancellation Metrics Track these metrics for process improvement: - Cancellation rate by status - Time between creation and cancellation - Most common cancellation reasons - Cancellations by client or sales agent - Conversion rate after cancellation (new proforma created) ### Process Improvement Use cancellation data to: - Identify training needs - Improve validation workflows - Reduce errors in proforma creation - Optimize pricing strategies - Better qualify leads before sending proformas ## Recovery After Cancellation Common next steps after cancellation: 1. **Create new proforma** - With corrected information 2. **Close opportunity** - If project is truly cancelled 3. **Schedule follow-up** - If project is postponed 4. **Negotiate terms** - If cancellation was due to disagreement 5. **Learn from errors** - If cancellation was due to mistake --- ## Convert Proforma to Invoice > Convert a proforma invoice into a final invoice URL: https://docs.storno.ro/api-reference/proforma-invoices/convert # Convert Proforma to Invoice Converts a proforma invoice into a final, legally-binding invoice. This action creates a new invoice with all the proforma's data, marks the proforma as `converted`, and establishes a link between the two documents. Once converted, the proforma cannot be modified and serves as a historical reference to the original quote. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the proforma invoice to convert | ## Request Body (Optional) | Field | Type | Required | Description | |-------|------|----------|-------------| | `invoiceSeriesId` | string | No | UUID of invoice series (if different from proforma series) | | `issueDate` | string | No | Override issue date (default: today) | | `dueDate` | string | No | Override due date (default: proforma's due date) | | `overrideFields` | object | No | Fields to override from the proforma data | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000/convert \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "invoiceSeriesId": "660e8400-e29b-41d4-a716-446655440000", "issueDate": "2026-02-18", "dueDate": "2026-03-18" }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000/convert', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ invoiceSeriesId: '660e8400-e29b-41d4-a716-446655440000', issueDate: '2026-02-18', dueDate: '2026-03-18' }) }); const data = await response.json(); ``` ## Response Returns the newly created invoice object along with updated proforma status: ```json { "invoice": { "uuid": "650e8400-e29b-41d4-a716-446655440111", "number": "FAC-2026-045", "direction": "outgoing", "isCreditNote": false, "seriesId": "660e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "660e8400-e29b-41d4-a716-446655440000", "name": "FAC", "nextNumber": 46, "prefix": "FAC-", "year": 2026 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "address": "Str. Exemplu 123, București", "email": "contact@client.ro", "phone": "+40721234567" }, "status": "draft", "issueDate": "2026-02-18", "dueDate": "2026-03-18", "currency": "RON", "exchangeRate": 1.0, "invoiceTypeCode": "380", "notes": "Payment terms: 30 days", "paymentTerms": "Net 30", "deliveryLocation": "Client warehouse", "projectReference": "PROJECT-2026-001", "orderNumber": "PO-2026-123", "contractNumber": "CONTRACT-2026-456", "proformaReference": "PRO-2026-001", "proformaId": "550e8400-e29b-41d4-a716-446655440000", "lines": [ { "uuid": "980e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Web Development Services - Phase 1", "quantity": "40.00", "unitPrice": "150.00", "unitOfMeasure": "hour", "productId": "450e8400-e29b-41d4-a716-446655440000", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "percentage": "19.00" }, "discount": "0.00", "vatIncluded": false, "subtotal": "6000.00", "vatAmount": "1140.00", "total": "7140.00" }, { "uuid": "990e8400-e29b-41d4-a716-446655440000", "lineNumber": 2, "description": "Hosting Services - Annual", "quantity": "1.00", "unitPrice": "1200.00", "unitOfMeasure": "service", "productId": "460e8400-e29b-41d4-a716-446655440000", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "percentage": "19.00" }, "discount": "200.00", "vatIncluded": false, "subtotal": "1000.00", "vatAmount": "190.00", "total": "1190.00" } ], "subtotal": "7000.00", "totalDiscount": "200.00", "vatAmount": "1330.00", "total": "8330.00", "anafStatus": null, "anafUploadIndex": null, "createdAt": "2026-02-18T15:00:00Z", "updatedAt": "2026-02-18T15:00:00Z" }, "proforma": { "uuid": "550e8400-e29b-41d4-a716-446655440000", "number": "PRO-2026-001", "status": "converted", "convertedAt": "2026-02-18T15:00:00Z", "convertedInvoiceId": "650e8400-e29b-41d4-a716-446655440111", "convertedInvoiceNumber": "FAC-2026-045", "updatedAt": "2026-02-18T15:00:00Z" } } ``` ## State Changes ### Proforma Changes - **Status:** Changed from `draft`, `sent`, or `accepted` → `converted` - **convertedAt:** Set to current UTC timestamp - **convertedInvoiceId:** Set to the UUID of the created invoice - **convertedInvoiceNumber:** Set to the invoice number for easy reference ### Invoice Creation - New invoice created with status `draft` - All line items copied from proforma - Client, pricing, and terms copied from proforma - `proformaId` and `proformaReference` fields set to link back to proforma - Ready to be uploaded to ANAF ## Data Mapping The following fields are copied from proforma to invoice: ### Basic Information - `clientId` - Same client - `currency` and `exchangeRate` - Same currency settings - `invoiceTypeCode` - Same invoice type ### Dates - `issueDate` - Defaults to today (can be overridden) - `dueDate` - Defaults to proforma's due date (can be overridden) ### References - `notes` - Public notes - `paymentTerms` - Payment terms - `deliveryLocation` - Delivery location - `projectReference` - Project reference - `orderNumber` - PO number - `contractNumber` - Contract number - `issuerName` and `issuerId` - Issuer information - `mentions` - Additional mentions - `salesAgent` - Sales agent ### Line Items - All line items with same details: - Description, quantity, unit price - VAT rate, unit of measure - Product reference - Discounts - VAT calculation method ### New Fields Added to Invoice - `proformaId` - UUID of source proforma - `proformaReference` - Proforma number for display ## Validation Rules ### Proforma Status Can convert proforma in these statuses: - `draft` - Can convert immediately - `sent` - Can convert without explicit acceptance - `accepted` - Recommended path for conversion Cannot convert proforma in these statuses: - `rejected` - Client declined - `cancelled` - Offer withdrawn - `converted` - Already converted ### Data Validation - All proforma data must be valid - Client must still exist - VAT rates must still exist - Products (if referenced) must still exist - Series must be valid for invoices ### Series Selection - If `invoiceSeriesId` not provided, uses proforma's series (if valid for invoices) - Series must be configured for outgoing invoices - Series must belong to the same company ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Proforma invoice not found or doesn't belong to the company | | 409 | `conflict` | Proforma status prevents conversion or already converted | | 422 | `validation_error` | Invalid request body or proforma data cannot be converted | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict - Already Converted ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be converted", "details": { "status": "converted", "reason": "Proforma invoice has already been converted to an invoice", "convertedAt": "2026-02-18T15:00:00Z", "convertedInvoiceId": "650e8400-e29b-41d4-a716-446655440111", "convertedInvoiceNumber": "FAC-2026-045" } } } ``` ### Status Conflict - Rejected ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be converted", "details": { "status": "rejected", "reason": "Cannot convert rejected proforma to invoice", "rejectedAt": "2026-02-17T11:20:00Z" } } } ``` ### Status Conflict - Cancelled ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be converted", "details": { "status": "cancelled", "reason": "Cannot convert cancelled proforma to invoice", "cancelledAt": "2026-02-17T14:45:00Z" } } } ``` ## Workflow Integration ### Standard Conversion Flow 1. Create proforma (`POST /api/v1/proforma-invoices`) 2. Send to client (`POST /api/v1/proforma-invoices/{uuid}/send`) 3. Client accepts (`POST /api/v1/proforma-invoices/{uuid}/accept`) 4. **Convert to invoice** (`POST /api/v1/proforma-invoices/{uuid}/convert`) ← You are here 5. Upload invoice to ANAF (`POST /api/v1/invoices/{uuid}/upload`) 6. Send invoice to client ### Fast-Track Flow For trusted clients or standard orders: 1. Create proforma 2. **Convert immediately** (skip send/accept steps) 3. Upload to ANAF 4. Send to client ## Post-Conversion Actions After conversion, you should: 1. **Upload to ANAF** - Submit the invoice to ANAF e-Factura system 2. **Generate PDF** - Create PDF version for client 3. **Send to client** - Email invoice to client 4. **Update CRM** - Sync invoice status to CRM 5. **Track payment** - Monitor payment against this invoice 6. **Archive proforma** - Keep proforma as reference ## Best Practices 1. **Review before converting** - Ensure all proforma data is correct 2. **Choose correct series** - Use appropriate invoice series 3. **Set correct dates** - Issue date typically = today 4. **Link documents** - Proforma reference is automatically maintained 5. **Upload promptly** - Submit to ANAF within required timeframe 6. **Notify stakeholders** - Alert accounting and sales teams 7. **Monitor conversion metrics** - Track proforma-to-invoice conversion rate ## Conversion Metrics Track these metrics for sales performance: - **Conversion rate** - % of proformas converted to invoices - **Time to conversion** - Days from send to conversion - **Conversion by status** - Direct vs accepted vs draft - **Value conversion** - Total value of converted vs rejected proformas - **Sales agent performance** - Conversion rate by sales agent ## Reversing a Conversion If the invoice needs to be cancelled after conversion: 1. **Cannot "unconvert"** - The conversion is permanent 2. **Use credit note** - Create a credit note to reverse the invoice 3. **Proforma remains converted** - Original proforma status doesn't change 4. **Create new proforma** - If needed for revised offer The bidirectional link between proforma and invoice is maintained for audit trail purposes. --- ## Create Proforma Invoice > Create a new proforma invoice with line items URL: https://docs.storno.ro/api-reference/proforma-invoices/create # Create Proforma Invoice Creates a new proforma invoice in draft status. The proforma can be edited until it's sent, accepted, rejected, or converted to a final invoice. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `clientId` | string | Yes | UUID of the client | | `seriesId` | string | Yes | UUID of the invoice series | | `issueDate` | string | Yes | Date of issue (YYYY-MM-DD) | | `dueDate` | string | Yes | Payment due date (YYYY-MM-DD) | | `validUntil` | string | Yes | Valid until date (YYYY-MM-DD) | | `currency` | string | Yes | Currency code (RON, EUR, USD, etc.) | | `exchangeRate` | number | No | Exchange rate (defaults to 1.0 for RON) | | `invoiceTypeCode` | string | No | Invoice type code (default: "380" - Commercial Invoice) | | `notes` | string | No | Public notes visible to client | | `paymentTerms` | string | No | Payment terms description (e.g., "Net 30") | | `deliveryLocation` | string | No | Delivery address or location | | `projectReference` | string | No | Related project reference | | `orderNumber` | string | No | Client purchase order number | | `contractNumber` | string | No | Related contract number | | `issuerName` | string | No | Name of person issuing the proforma | | `issuerId` | string | No | UUID of the issuer user | | `mentions` | string | No | Additional mentions or notes | | `internalNote` | string | No | Internal note (not visible to client) | | `salesAgent` | string | No | Sales agent name | | `language` | string | No | Document language for PDF generation: `ro`, `en`, `de`, `fr` (default: `ro`) | | `lines` | array | Yes | Array of line items (minimum 1 item) | ### Line Item Object | Field | Type | Required | Description | |-------|------|----------|-------------| | `description` | string | Yes | Item description | | `quantity` | number | Yes | Quantity (must be > 0) | | `unitPrice` | number | Yes | Price per unit | | `vatRateId` | string | Yes | UUID of the VAT rate | | `unitOfMeasure` | string | No | Unit of measure (e.g., hour, piece, kg) | | `productId` | string | No | UUID of related product | | `discount` | number | No | Absolute discount amount | | `discountPercent` | number | No | Discount percentage (0-100) | | `vatIncluded` | boolean | No | Whether unit price includes VAT (default: false) | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/proforma-invoices \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "clientId": "750e8400-e29b-41d4-a716-446655440000", "seriesId": "650e8400-e29b-41d4-a716-446655440000", "issueDate": "2026-02-16", "dueDate": "2026-03-16", "validUntil": "2026-03-16", "currency": "RON", "exchangeRate": 1.0, "invoiceTypeCode": "380", "notes": "Payment terms: 30 days from invoice date", "paymentTerms": "Net 30", "deliveryLocation": "Client warehouse", "projectReference": "PROJECT-2026-001", "orderNumber": "PO-2026-123", "contractNumber": "CONTRACT-2026-456", "issuerName": "John Doe", "mentions": "Special delivery instructions", "internalNote": "VIP client - priority handling", "salesAgent": "Jane Smith", "lines": [ { "description": "Web Development Services - Phase 1", "quantity": 40, "unitPrice": 150, "unitOfMeasure": "hour", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "450e8400-e29b-41d4-a716-446655440000", "discount": 0, "discountPercent": 0, "vatIncluded": false }, { "description": "Hosting Services - Annual", "quantity": 1, "unitPrice": 1200, "unitOfMeasure": "service", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "460e8400-e29b-41d4-a716-446655440000", "discount": 200, "discountPercent": 16.67, "vatIncluded": false } ] }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/proforma-invoices', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: '750e8400-e29b-41d4-a716-446655440000', seriesId: '650e8400-e29b-41d4-a716-446655440000', issueDate: '2026-02-16', dueDate: '2026-03-16', validUntil: '2026-03-16', currency: 'RON', exchangeRate: 1.0, invoiceTypeCode: '380', notes: 'Payment terms: 30 days from invoice date', paymentTerms: 'Net 30', deliveryLocation: 'Client warehouse', projectReference: 'PROJECT-2026-001', orderNumber: 'PO-2026-123', contractNumber: 'CONTRACT-2026-456', issuerName: 'John Doe', mentions: 'Special delivery instructions', internalNote: 'VIP client - priority handling', salesAgent: 'Jane Smith', lines: [ { description: 'Web Development Services - Phase 1', quantity: 40, unitPrice: 150, unitOfMeasure: 'hour', vatRateId: '350e8400-e29b-41d4-a716-446655440000', productId: '450e8400-e29b-41d4-a716-446655440000', discount: 0, discountPercent: 0, vatIncluded: false }, { description: 'Hosting Services - Annual', quantity: 1, unitPrice: 1200, unitOfMeasure: 'service', vatRateId: '350e8400-e29b-41d4-a716-446655440000', productId: '460e8400-e29b-41d4-a716-446655440000', discount: 200, discountPercent: 16.67, vatIncluded: false } ] }) }); const data = await response.json(); ``` ## Response ```json { "uuid": "550e8400-e29b-41d4-a716-446655440000", "number": "PRO-2026-001", "seriesId": "650e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "650e8400-e29b-41d4-a716-446655440000", "name": "PRO", "nextNumber": 2, "prefix": "PRO-", "year": 2026 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "address": "Str. Exemplu 123, București", "email": "contact@client.ro", "phone": "+40721234567" }, "status": "draft", "issueDate": "2026-02-16", "dueDate": "2026-03-16", "validUntil": "2026-03-16", "currency": "RON", "exchangeRate": 1.0, "invoiceTypeCode": "380", "notes": "Payment terms: 30 days from invoice date", "paymentTerms": "Net 30", "deliveryLocation": "Client warehouse", "projectReference": "PROJECT-2026-001", "orderNumber": "PO-2026-123", "contractNumber": "CONTRACT-2026-456", "issuerName": "John Doe", "mentions": "Special delivery instructions", "internalNote": "VIP client - priority handling", "salesAgent": "Jane Smith", "lines": [ { "uuid": "950e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Web Development Services - Phase 1", "quantity": "40.00", "unitPrice": "150.00", "unitOfMeasure": "hour", "productId": "450e8400-e29b-41d4-a716-446655440000", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "discount": "0.00", "discountPercent": "0.00", "vatIncluded": false, "subtotal": "6000.00", "vatAmount": "1140.00", "total": "7140.00" }, { "uuid": "960e8400-e29b-41d4-a716-446655440000", "lineNumber": 2, "description": "Hosting Services - Annual", "quantity": "1.00", "unitPrice": "1200.00", "unitOfMeasure": "service", "productId": "460e8400-e29b-41d4-a716-446655440000", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "discount": "200.00", "discountPercent": "16.67", "vatIncluded": false, "subtotal": "1000.00", "vatAmount": "190.00", "total": "1190.00" } ], "subtotal": "7000.00", "totalDiscount": "200.00", "vatAmount": "1330.00", "total": "8330.00", "sentAt": null, "acceptedAt": null, "rejectedAt": null, "cancelledAt": null, "convertedAt": null, "convertedInvoiceId": null, "createdAt": "2026-02-16T09:00:00Z", "updatedAt": "2026-02-16T09:00:00Z" } ``` ## Validation Rules ### Dates - `issueDate` must be a valid date in YYYY-MM-DD format - `dueDate` must be equal to or after `issueDate` - `validUntil` must be equal to or after `issueDate` ### Currency - Must be a valid 3-letter currency code (ISO 4217) - `exchangeRate` must be greater than 0 - For RON (base currency), `exchangeRate` defaults to 1.0 ### Line Items - Minimum 1 line item required - `quantity` must be greater than 0 - `unitPrice` must be greater than or equal to 0 - Either `discount` or `discountPercent` can be provided (not both) - If `discountPercent` is provided, it must be between 0 and 100 - `vatRateId` must reference an existing VAT rate - If `productId` is provided, it must reference an existing product ### References - `clientId` must reference an existing client - `seriesId` must reference an existing series configured for proforma invoices - If `issuerId` is provided, it must reference an existing user ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid request body structure | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Referenced entity not found (client, series, VAT rate, product, user) | | 422 | `validation_error` | Validation failed (see error details for specific field errors) | | 500 | `internal_error` | Server error occurred | ## Example Validation Error Response ```json { "error": { "code": "validation_error", "message": "Validation failed", "details": { "clientId": ["Client not found"], "lines.0.quantity": ["Quantity must be greater than 0"], "lines.1.vatRateId": ["VAT rate not found"], "dueDate": ["Due date must be after issue date"] } } } ``` --- ## Delete Proforma Invoice > Permanently delete a proforma invoice (draft status only) URL: https://docs.storno.ro/api-reference/proforma-invoices/delete # Delete Proforma Invoice Permanently deletes a proforma invoice and all its line items. Only proforma invoices in `draft` status can be deleted. Once sent, accepted, rejected, converted, or cancelled, a proforma cannot be deleted. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the proforma invoice to delete | ## Request ```bash {% title="cURL" %} curl -X DELETE https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000', { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); // Success: 204 No Content (no response body) if (response.status === 204) { console.log('Proforma invoice deleted successfully'); } ``` ## Response **Success:** Returns `204 No Content` with an empty response body. The proforma invoice and all associated line items are permanently deleted from the database. ## Restrictions ### Status Requirement Only proforma invoices with `status = draft` can be deleted. Proforma invoices in the following states **cannot** be deleted: - `sent` - Already sent to client - `accepted` - Client has accepted - `rejected` - Client has rejected - `converted` - Converted to invoice - `cancelled` - Already cancelled For non-draft proforma invoices, use the [cancel endpoint](/api-reference/proforma-invoices/cancel) instead to mark them as cancelled without deleting historical data. ### Referential Integrity - Deleting a proforma does not affect the invoice number series - The series counter is not rolled back - If the proforma was converted to an invoice, the invoice is **not** deleted ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Proforma invoice not found or doesn't belong to the company | | 409 | `conflict` | Proforma status prevents deletion (not in draft) | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be deleted", "details": { "status": "sent", "reason": "Only draft proforma invoices can be deleted. Use cancel instead." } } } ``` ### Not Found ```json { "error": { "code": "not_found", "message": "Proforma invoice not found" } } ``` ## Best Practices ### When to Delete vs Cancel **Delete** when: - The proforma was created by mistake - The proforma is still in draft and hasn't been shared - You want to remove all traces of the document **Cancel** when: - The proforma has been sent to the client - You need to maintain audit trail - The proforma was accepted/rejected but the deal fell through - You need historical records for reporting ### Audit Considerations Deleted proforma invoices: - Are permanently removed from the database - Do not appear in reports or exports - Cannot be recovered - Do not leave audit trail entries For compliance and audit purposes, consider using the cancel endpoint instead of delete, especially for proforma invoices that were shared externally. --- ## Get Proforma Invoice > Retrieve detailed information for a specific proforma invoice including line items URL: https://docs.storno.ro/api-reference/proforma-invoices/get # Get Proforma Invoice Retrieves complete details for a specific proforma invoice, including all line items, client information, and calculated totals. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the proforma invoice to retrieve | ## Request ```bash {% title="cURL" %} curl -X GET https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response ```json { "uuid": "550e8400-e29b-41d4-a716-446655440000", "number": "PRO-2026-001", "seriesId": "650e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "650e8400-e29b-41d4-a716-446655440000", "name": "PRO", "nextNumber": 2, "prefix": "PRO-", "year": 2026 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "address": "Str. Exemplu 123, București, Sector 1", "city": "București", "county": "București", "country": "RO", "postalCode": "010101", "email": "contact@client.ro", "phone": "+40721234567", "bankAccount": "RO49AAAA1B31007593840000", "bankName": "Banca Comercială Română" }, "status": "sent", "issueDate": "2026-02-16", "dueDate": "2026-03-16", "validUntil": "2026-03-16", "currency": "RON", "exchangeRate": 1.0, "invoiceTypeCode": "380", "notes": "Payment terms: 30 days from invoice date", "paymentTerms": "Net 30", "deliveryLocation": "Client warehouse - Str. Depozit 5, București", "projectReference": "PROJECT-2026-001", "orderNumber": "PO-2026-123", "contractNumber": "CONTRACT-2026-456", "issuerName": "John Doe", "issuerId": "850e8400-e29b-41d4-a716-446655440000", "mentions": "Special delivery instructions: Handle with care", "internalNote": "Internal reference note - VIP client", "salesAgent": "Jane Smith", "lines": [ { "uuid": "950e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Web Development Services - Phase 1", "quantity": "40.00", "unitPrice": "150.00", "unitOfMeasure": "hour", "productId": "450e8400-e29b-41d4-a716-446655440000", "product": { "uuid": "450e8400-e29b-41d4-a716-446655440000", "name": "Web Development Services", "code": "WEB-DEV-001", "unitOfMeasure": "hour" }, "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "discount": "0.00", "discountPercent": "0.00", "vatIncluded": false, "subtotal": "6000.00", "vatAmount": "1140.00", "total": "7140.00" }, { "uuid": "960e8400-e29b-41d4-a716-446655440000", "lineNumber": 2, "description": "Hosting Services - Annual", "quantity": "1.00", "unitPrice": "1200.00", "unitOfMeasure": "service", "productId": "460e8400-e29b-41d4-a716-446655440000", "product": { "uuid": "460e8400-e29b-41d4-a716-446655440000", "name": "Hosting Services", "code": "HOST-001", "unitOfMeasure": "service" }, "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "discount": "200.00", "discountPercent": "16.67", "vatIncluded": false, "subtotal": "1000.00", "vatAmount": "190.00", "total": "1190.00" } ], "subtotal": "7000.00", "totalDiscount": "200.00", "vatAmount": "1330.00", "total": "8330.00", "sentAt": "2026-02-16T10:30:00Z", "acceptedAt": null, "rejectedAt": null, "cancelledAt": null, "convertedAt": null, "convertedInvoiceId": null, "createdAt": "2026-02-16T09:00:00Z", "updatedAt": "2026-02-16T10:30:00Z" } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `number` | string | Proforma invoice number | | `status` | string | Current status (draft/sent/accepted/rejected/converted/cancelled) | | `series` | object | Series information with prefix and year | | `client` | object | Complete client details including banking information | | `lines` | array | Array of line items with products, pricing, and VAT calculations | | `subtotal` | string | Subtotal before VAT | | `totalDiscount` | string | Sum of all line item discounts | | `vatAmount` | string | Total VAT amount | | `total` | string | Grand total including VAT | | `issueDate` | string | Date of issue | | `dueDate` | string | Payment due date | | `validUntil` | string | Proforma validity date | | `currency` | string | Currency code | | `exchangeRate` | number | Exchange rate to base currency | | `invoiceTypeCode` | string | Invoice type code (ANAF standard) | | `paymentTerms` | string | Payment terms description | | `deliveryLocation` | string | Delivery address or location | | `projectReference` | string | Related project reference | | `orderNumber` | string | Client purchase order number | | `contractNumber` | string | Related contract number | | `issuerName` | string | Name of person who issued the proforma | | `issuerId` | string | UUID of the issuer user | | `salesAgent` | string | Sales agent name | | `mentions` | string | Additional mentions or notes | | `internalNote` | string | Internal note (not shown to client) | | `sentAt` | string \| null | Timestamp when sent | | `acceptedAt` | string \| null | Timestamp when accepted | | `rejectedAt` | string \| null | Timestamp when rejected | | `cancelledAt` | string \| null | Timestamp when cancelled | | `convertedAt` | string \| null | Timestamp when converted to invoice | | `convertedInvoiceId` | string \| null | UUID of created invoice (if converted) | ### Line Item Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Line item unique identifier | | `lineNumber` | integer | Sequential line number | | `description` | string | Item description | | `quantity` | string | Quantity (decimal string) | | `unitPrice` | string | Price per unit | | `unitOfMeasure` | string | Unit of measure (e.g., hour, piece, service) | | `product` | object \| null | Related product details | | `vatRate` | object | VAT rate details with percentage | | `discount` | string | Absolute discount amount | | `discountPercent` | string | Discount as percentage | | `vatIncluded` | boolean | Whether unit price includes VAT | | `subtotal` | string | Line subtotal (after discount, before VAT) | | `vatAmount` | string | Line VAT amount | | `total` | string | Line total (including VAT) | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Proforma invoice not found or doesn't belong to the specified company | | 500 | `internal_error` | Server error occurred | --- ## List Proforma Invoices > Retrieve a paginated list of proforma invoices with optional filtering and search URL: https://docs.storno.ro/api-reference/proforma-invoices/list # List Proforma Invoices Retrieves a paginated list of proforma invoices for the authenticated company. Supports filtering by status, date range, client, and full-text search across invoice numbers and client names. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Query Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `page` | integer | No | Page number (default: 1) | | `limit` | integer | No | Items per page (default: 20, max: 100) | | `search` | string | No | Search term for invoice number or client name | | `status` | string | No | Filter by status: `draft`, `sent`, `accepted`, `rejected`, `converted`, `cancelled` | | `from` | string | No | Start date filter (ISO 8601 format: YYYY-MM-DD) | | `to` | string | No | End date filter (ISO 8601 format: YYYY-MM-DD) | | `clientId` | string | No | Filter by client UUID | ## Request ```bash {% title="cURL" %} curl -X GET https://api.storno.ro/api/v1/proforma-invoices?page=1&limit=20&status=sent \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/proforma-invoices?page=1&limit=20&status=sent', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response ```json { "data": [ { "uuid": "550e8400-e29b-41d4-a716-446655440000", "number": "PRO-2026-001", "seriesId": "650e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "650e8400-e29b-41d4-a716-446655440000", "name": "PRO", "nextNumber": 2 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "address": "Str. Exemplu 123, București" }, "status": "sent", "issueDate": "2026-02-16", "dueDate": "2026-03-16", "validUntil": "2026-03-16", "currency": "RON", "exchangeRate": 1.0, "subtotal": "1000.00", "vatAmount": "190.00", "total": "1190.00", "notes": "Payment terms: 30 days", "paymentTerms": "Net 30", "deliveryLocation": "Client warehouse", "projectReference": "PROJECT-2026-001", "orderNumber": "PO-2026-123", "contractNumber": "CONTRACT-2026-456", "issuerName": "John Doe", "issuerId": "850e8400-e29b-41d4-a716-446655440000", "mentions": "Special delivery instructions", "internalNote": "Internal reference note", "salesAgent": "Jane Smith", "sentAt": "2026-02-16T10:30:00Z", "acceptedAt": null, "rejectedAt": null, "cancelledAt": null, "convertedAt": null, "convertedInvoiceId": null, "createdAt": "2026-02-16T09:00:00Z", "updatedAt": "2026-02-16T10:30:00Z" } ], "total": 45, "page": 1, "limit": 20, "pages": 3 } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `data` | array | Array of proforma invoice objects | | `total` | integer | Total number of proforma invoices matching the filters | | `page` | integer | Current page number | | `limit` | integer | Items per page | | `pages` | integer | Total number of pages | ### Proforma Invoice Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `number` | string | Proforma invoice number (e.g., PRO-2026-001) | | `status` | string | Status: `draft`, `sent`, `accepted`, `rejected`, `converted`, `cancelled` | | `issueDate` | string | Date of issue (YYYY-MM-DD) | | `dueDate` | string | Payment due date (YYYY-MM-DD) | | `validUntil` | string | Valid until date (YYYY-MM-DD) | | `currency` | string | Currency code (e.g., RON, EUR, USD) | | `exchangeRate` | number | Exchange rate to company base currency | | `subtotal` | string | Subtotal amount (excluding VAT) | | `vatAmount` | string | Total VAT amount | | `total` | string | Total amount (including VAT) | | `client` | object | Client details | | `series` | object | Series details | | `sentAt` | string \| null | ISO 8601 timestamp when marked as sent | | `acceptedAt` | string \| null | ISO 8601 timestamp when accepted | | `rejectedAt` | string \| null | ISO 8601 timestamp when rejected | | `cancelledAt` | string \| null | ISO 8601 timestamp when cancelled | | `convertedAt` | string \| null | ISO 8601 timestamp when converted to invoice | | `convertedInvoiceId` | string \| null | UUID of the created invoice (if converted) | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 422 | `validation_error` | Invalid query parameters (e.g., invalid status value, invalid date format) | | 500 | `internal_error` | Server error occurred | ## Status Lifecycle Proforma invoices follow this status flow: - **draft** → Initial state when created - **sent** → Marked as sent to client - **accepted** → Client accepted the proforma - **rejected** → Client rejected the proforma - **converted** → Converted to a final invoice - **cancelled** → Proforma was cancelled Once a proforma is `converted`, `cancelled`, `accepted`, or `rejected`, it cannot be modified. --- ## Reject Proforma Invoice > Mark a proforma invoice as rejected by the client URL: https://docs.storno.ro/api-reference/proforma-invoices/reject # Reject Proforma Invoice Marks a proforma invoice as rejected by the client. This action transitions the proforma to `rejected` status and records the rejection timestamp. Once rejected, the proforma cannot be converted to an invoice and serves as a historical record of the declined offer. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the proforma invoice to mark as rejected | ## Request Body (Optional) | Field | Type | Required | Description | |-------|------|----------|-------------| | `rejectionReason` | string | No | Reason for rejection provided by the client | | `rejectionNotes` | string | No | Internal notes about the rejection | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000/reject \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "rejectionReason": "Price too high", "rejectionNotes": "Client requested 15% discount, consider follow-up" }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000/reject', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ rejectionReason: 'Price too high', rejectionNotes: 'Client requested 15% discount, consider follow-up' }) }); const data = await response.json(); ``` ## Response Returns the updated proforma invoice with `status = rejected` and `rejectedAt` timestamp: ```json { "uuid": "550e8400-e29b-41d4-a716-446655440000", "number": "PRO-2026-001", "seriesId": "650e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "650e8400-e29b-41d4-a716-446655440000", "name": "PRO", "nextNumber": 2 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "email": "contact@client.ro" }, "status": "rejected", "issueDate": "2026-02-16", "dueDate": "2026-03-16", "validUntil": "2026-03-16", "currency": "RON", "exchangeRate": 1.0, "subtotal": "7000.00", "vatAmount": "1330.00", "total": "8330.00", "notes": "Payment terms: 30 days", "paymentTerms": "Net 30", "rejectionReason": "Price too high", "rejectionNotes": "Client requested 15% discount, consider follow-up", "sentAt": "2026-02-16T10:30:00Z", "acceptedAt": null, "rejectedAt": "2026-02-17T11:20:00Z", "cancelledAt": null, "convertedAt": null, "convertedInvoiceId": null, "createdAt": "2026-02-16T09:00:00Z", "updatedAt": "2026-02-17T11:20:00Z" } ``` ## State Changes ### Status Transition - **Before:** `status = sent` (or `draft`) - **After:** `status = rejected` ### Timestamp - Sets `rejectedAt` to current UTC timestamp (ISO 8601 format) - Updates `updatedAt` timestamp ### Restrictions Applied Once rejected: - Cannot be converted to invoice - Cannot be accepted - Cannot be edited or deleted - Serves as historical record only ## Validation Rules ### Status Requirement - Proforma must have `status = sent` or `status = draft` - Cannot reject a proforma that is already accepted, rejected, converted, or cancelled ### Business Logic Rejecting a proforma indicates: - Client declines the offer - No invoice will be generated from this proforma - Opportunity to follow up with revised offer - Record preserved for sales analytics ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Proforma invoice not found or doesn't belong to the company | | 409 | `conflict` | Proforma status prevents rejection | | 422 | `validation_error` | Invalid request body (if rejection reason/notes provided) | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict - Already Rejected ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be rejected", "details": { "status": "rejected", "reason": "Proforma invoice is already rejected", "rejectedAt": "2026-02-17T11:20:00Z" } } } ``` ### Status Conflict - Already Accepted ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be rejected", "details": { "status": "accepted", "reason": "Proforma invoice has already been accepted by the client", "acceptedAt": "2026-02-17T09:15:00Z" } } } ``` ### Status Conflict - Already Converted ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be rejected", "details": { "status": "converted", "reason": "Proforma invoice has already been converted to an invoice", "convertedAt": "2026-02-17T10:00:00Z", "convertedInvoiceId": "650e8400-e29b-41d4-a716-446655440111" } } } ``` ## Workflow Integration ### Typical Rejection Flow 1. Send proforma to client (`POST /api/v1/proforma-invoices/{uuid}/send`) 2. Client reviews and declines 3. **Mark as rejected** (`POST /api/v1/proforma-invoices/{uuid}/reject`) ← You are here 4. Create new proforma with revised terms (optional) 5. Update CRM with rejection reason 6. Schedule follow-up actions ### Client Portal Integration If you have a client portal: - Allow clients to reject proformas with reason - Capture rejection reason from client directly - Trigger notification to sales team - Offer option to request revised quote ## Rejection Tracking ### Analytics Use Cases Rejected proformas provide valuable insights: - **Win/loss analysis** - Track rejection reasons - **Pricing optimization** - Identify price sensitivity - **Competitive analysis** - Understand why clients choose competitors - **Sales training** - Learn from rejection patterns - **Product fit** - Identify mismatched offerings ### Common Rejection Reasons - Price too high - Timeline not suitable - Features don't match requirements - Found alternative supplier - Budget constraints - Project cancelled/postponed - Terms and conditions unacceptable - Delivery location not supported ## Best Practices 1. **Always capture rejection reason** - Critical for sales intelligence 2. **Notify stakeholders** - Alert sales team immediately 3. **Update CRM** - Sync rejection status and reason 4. **Schedule follow-up** - Set reminder to reach out with revised offer 5. **Analyze patterns** - Review rejection reasons monthly 6. **Preserve data** - Keep rejected proformas for reporting 7. **Learn and adapt** - Use feedback to improve future offers ## Rejection vs Cancellation **Reject** when: - Client explicitly declines the offer - Client decides not to proceed - Client provides feedback or reason - You want to track client-driven rejections **Cancel** when: - You want to withdraw the offer - Terms changed and proforma is no longer valid - Error in original proforma - You want to track company-driven cancellations Both statuses prevent conversion to invoice, but help distinguish between client-driven and company-driven outcomes. ## Recovery from Rejection After rejection, you can: 1. Create a new proforma with revised terms 2. Offer discount or better payment terms 3. Adjust scope to match budget 4. Schedule follow-up meeting 5. Keep in pipeline for future opportunities The rejected proforma remains accessible for reference when creating the revised offer. --- ## Send Proforma Invoice > Mark a proforma invoice as sent to the client URL: https://docs.storno.ro/api-reference/proforma-invoices/send # Send Proforma Invoice Marks a proforma invoice as sent to the client. This action transitions the proforma from `draft` status to `sent` status and records the timestamp when it was sent. Once sent, the proforma becomes read-only and can no longer be edited or deleted. It can only be accepted, rejected, cancelled, or converted to an invoice. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the proforma invoice to mark as sent | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000/send \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000/send', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response Returns the updated proforma invoice with `status = sent` and `sentAt` timestamp: ```json { "uuid": "550e8400-e29b-41d4-a716-446655440000", "number": "PRO-2026-001", "seriesId": "650e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "650e8400-e29b-41d4-a716-446655440000", "name": "PRO", "nextNumber": 2 }, "clientId": "750e8400-e29b-41d4-a716-446655440000", "client": { "uuid": "750e8400-e29b-41d4-a716-446655440000", "name": "Client SRL", "registrationNumber": "RO12345678", "email": "contact@client.ro" }, "status": "sent", "issueDate": "2026-02-16", "dueDate": "2026-03-16", "validUntil": "2026-03-16", "currency": "RON", "exchangeRate": 1.0, "subtotal": "7000.00", "vatAmount": "1330.00", "total": "8330.00", "notes": "Payment terms: 30 days", "paymentTerms": "Net 30", "sentAt": "2026-02-16T14:30:00Z", "acceptedAt": null, "rejectedAt": null, "cancelledAt": null, "convertedAt": null, "convertedInvoiceId": null, "createdAt": "2026-02-16T09:00:00Z", "updatedAt": "2026-02-16T14:30:00Z" } ``` ## State Changes ### Status Transition - **Before:** `status = draft` - **After:** `status = sent` ### Timestamp - Sets `sentAt` to current UTC timestamp (ISO 8601 format) - Updates `updatedAt` timestamp ### Restrictions Applied Once marked as sent: - Cannot be updated (PUT requests will fail) - Cannot be deleted (DELETE requests will fail) - Can be accepted, rejected, cancelled, or converted ## Validation Rules ### Status Requirement - Proforma must have `status = draft` - Cannot send a proforma that is already sent, accepted, rejected, converted, or cancelled ### Data Completeness Before sending, ensure the proforma has: - Valid client information - At least one line item - All required fields populated - Correct totals calculated ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Proforma invoice not found or doesn't belong to the company | | 409 | `conflict` | Proforma status prevents sending (not in draft status) | | 422 | `validation_error` | Proforma data is incomplete or invalid | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be sent", "details": { "status": "sent", "reason": "Proforma invoice is already marked as sent", "sentAt": "2026-02-16T10:30:00Z" } } } ``` ### Validation Error ```json { "error": { "code": "validation_error", "message": "Proforma invoice data is incomplete", "details": { "client.email": ["Client email is required before sending"], "lines": ["At least one line item is required"] } } } ``` ## Integration Notes ### Email Integration This endpoint only changes the status in the database. To actually send the proforma via email, you should: 1. Call this endpoint to mark as sent 2. Use the [Email Sending API](/api-reference/emails/send) to deliver the PDF to the client 3. The email API will automatically attach the generated PDF ### Workflow Integration After marking as sent, you may want to: - Generate and download the PDF for your records - Send email notification to the client - Set up reminders for follow-up - Track when the client views/downloads the document ### Reversibility There is no "unsend" action. Once sent, the proforma remains in sent status until: - Client accepts it (→ `accepted`) - Client rejects it (→ `rejected`) - You cancel it (→ `cancelled`) - You convert it to an invoice (→ `converted`) ## Best Practices 1. **Validate before sending** - Review all data before marking as sent 2. **Email immediately** - Send the email right after this API call 3. **Log the action** - Record who sent it and when in your application logs 4. **Notify stakeholders** - Alert relevant team members that proforma was sent 5. **Set follow-up reminders** - Based on `validUntil` date --- ## Update Proforma Invoice > Update an existing proforma invoice (draft status only) URL: https://docs.storno.ro/api-reference/proforma-invoices/update # Update Proforma Invoice Updates an existing proforma invoice. Only proforma invoices in `draft` status can be updated. Once a proforma is sent, accepted, rejected, converted, or cancelled, it becomes immutable. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the proforma invoice to update | ## Request Body All fields from the create endpoint can be updated. The entire proforma is replaced with the new data. | Field | Type | Required | Description | |-------|------|----------|-------------| | `clientId` | string | Yes | UUID of the client | | `seriesId` | string | Yes | UUID of the invoice series | | `issueDate` | string | Yes | Date of issue (YYYY-MM-DD) | | `dueDate` | string | Yes | Payment due date (YYYY-MM-DD) | | `validUntil` | string | Yes | Valid until date (YYYY-MM-DD) | | `currency` | string | Yes | Currency code (RON, EUR, USD, etc.) | | `exchangeRate` | number | No | Exchange rate (defaults to 1.0 for RON) | | `invoiceTypeCode` | string | No | Invoice type code | | `notes` | string | No | Public notes visible to client | | `paymentTerms` | string | No | Payment terms description | | `deliveryLocation` | string | No | Delivery address or location | | `projectReference` | string | No | Related project reference | | `orderNumber` | string | No | Client purchase order number | | `contractNumber` | string | No | Related contract number | | `issuerName` | string | No | Name of person issuing the proforma | | `issuerId` | string | No | UUID of the issuer user | | `mentions` | string | No | Additional mentions or notes | | `internalNote` | string | No | Internal note (not visible to client) | | `salesAgent` | string | No | Sales agent name | | `language` | string | No | Document language for PDF generation: `ro`, `en`, `de`, `fr` (default: `ro`) | | `lines` | array | Yes | Array of line items (replaces all existing lines) | ### Line Item Object | Field | Type | Required | Description | |-------|------|----------|-------------| | `uuid` | string | No | UUID of existing line (if updating); omit to create new line | | `description` | string | Yes | Item description | | `quantity` | number | Yes | Quantity (must be > 0) | | `unitPrice` | number | Yes | Price per unit | | `vatRateId` | string | Yes | UUID of the VAT rate | | `unitOfMeasure` | string | No | Unit of measure | | `productId` | string | No | UUID of related product | | `discount` | number | No | Absolute discount amount | | `discountPercent` | number | No | Discount percentage (0-100) | | `vatIncluded` | boolean | No | Whether unit price includes VAT | ## Request ```bash {% title="cURL" %} curl -X PUT https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "clientId": "750e8400-e29b-41d4-a716-446655440000", "seriesId": "650e8400-e29b-41d4-a716-446655440000", "issueDate": "2026-02-16", "dueDate": "2026-03-20", "validUntil": "2026-03-20", "currency": "RON", "exchangeRate": 1.0, "notes": "Updated payment terms: 35 days", "paymentTerms": "Net 35", "deliveryLocation": "Updated delivery location", "projectReference": "PROJECT-2026-001", "orderNumber": "PO-2026-123", "issuerName": "John Doe", "salesAgent": "Jane Smith", "lines": [ { "uuid": "950e8400-e29b-41d4-a716-446655440000", "description": "Web Development Services - Phase 1 (Updated)", "quantity": 45, "unitPrice": 150, "unitOfMeasure": "hour", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "450e8400-e29b-41d4-a716-446655440000", "vatIncluded": false }, { "description": "Additional Services", "quantity": 10, "unitPrice": 100, "unitOfMeasure": "hour", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatIncluded": false } ] }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/proforma-invoices/550e8400-e29b-41d4-a716-446655440000', { method: 'PUT', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: '750e8400-e29b-41d4-a716-446655440000', seriesId: '650e8400-e29b-41d4-a716-446655440000', issueDate: '2026-02-16', dueDate: '2026-03-20', validUntil: '2026-03-20', currency: 'RON', exchangeRate: 1.0, notes: 'Updated payment terms: 35 days', paymentTerms: 'Net 35', deliveryLocation: 'Updated delivery location', projectReference: 'PROJECT-2026-001', orderNumber: 'PO-2026-123', issuerName: 'John Doe', salesAgent: 'Jane Smith', lines: [ { uuid: '950e8400-e29b-41d4-a716-446655440000', description: 'Web Development Services - Phase 1 (Updated)', quantity: 45, unitPrice: 150, unitOfMeasure: 'hour', vatRateId: '350e8400-e29b-41d4-a716-446655440000', productId: '450e8400-e29b-41d4-a716-446655440000', vatIncluded: false }, { description: 'Additional Services', quantity: 10, unitPrice: 100, unitOfMeasure: 'hour', vatRateId: '350e8400-e29b-41d4-a716-446655440000', vatIncluded: false } ] }) }); const data = await response.json(); ``` ## Response Returns the updated proforma invoice object with recalculated totals: ```json { "uuid": "550e8400-e29b-41d4-a716-446655440000", "number": "PRO-2026-001", "seriesId": "650e8400-e29b-41d4-a716-446655440000", "clientId": "750e8400-e29b-41d4-a716-446655440000", "status": "draft", "issueDate": "2026-02-16", "dueDate": "2026-03-20", "validUntil": "2026-03-20", "currency": "RON", "exchangeRate": 1.0, "notes": "Updated payment terms: 35 days", "paymentTerms": "Net 35", "deliveryLocation": "Updated delivery location", "projectReference": "PROJECT-2026-001", "orderNumber": "PO-2026-123", "issuerName": "John Doe", "salesAgent": "Jane Smith", "lines": [ { "uuid": "950e8400-e29b-41d4-a716-446655440000", "lineNumber": 1, "description": "Web Development Services - Phase 1 (Updated)", "quantity": "45.00", "unitPrice": "150.00", "unitOfMeasure": "hour", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "450e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "discount": "0.00", "vatIncluded": false, "subtotal": "6750.00", "vatAmount": "1282.50", "total": "8032.50" }, { "uuid": "970e8400-e29b-41d4-a716-446655440000", "lineNumber": 2, "description": "Additional Services", "quantity": "10.00", "unitPrice": "100.00", "unitOfMeasure": "hour", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "discount": "0.00", "vatIncluded": false, "subtotal": "1000.00", "vatAmount": "190.00", "total": "1190.00" } ], "subtotal": "7750.00", "totalDiscount": "0.00", "vatAmount": "1472.50", "total": "9222.50", "createdAt": "2026-02-16T09:00:00Z", "updatedAt": "2026-02-16T11:30:00Z" } ``` ## Line Item Behavior When updating lines: - Lines with existing `uuid` values are updated - Lines without `uuid` are created as new lines - Existing lines not included in the request are **deleted** - Line numbers are automatically reassigned sequentially ## Validation Rules Same validation rules apply as in the create endpoint: ### Status Restriction - Proforma must be in `draft` status - Cannot update proforma after it's sent, accepted, rejected, converted, or cancelled ### Dates - `issueDate` must be valid YYYY-MM-DD format - `dueDate` must be equal to or after `issueDate` - `validUntil` must be equal to or after `issueDate` ### Currency & Rates - `currency` must be valid ISO 4217 code - `exchangeRate` must be greater than 0 ### Line Items - Minimum 1 line required - All line validation rules from create endpoint apply ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid request body structure | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header, or proforma is not editable | | 404 | `not_found` | Proforma invoice not found or doesn't belong to the company | | 409 | `conflict` | Proforma status prevents updates (not in draft) | | 422 | `validation_error` | Validation failed (see error details) | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict ```json { "error": { "code": "conflict", "message": "Proforma invoice cannot be updated", "details": { "status": "sent", "reason": "Only draft proforma invoices can be updated" } } } ``` ### Validation Error ```json { "error": { "code": "validation_error", "message": "Validation failed", "details": { "dueDate": ["Due date must be after issue date"], "lines.0.quantity": ["Quantity must be greater than 0"], "lines": ["At least one line item is required"] } } } ``` --- ## Cancel Receipt > Cancel a receipt to void it from fiscal records URL: https://docs.storno.ro/api-reference/receipts/cancel # Cancel Receipt Cancels a receipt by transitioning it to `cancelled` status. Cancellation is used when a transaction must be voided — for example, when an item is returned, when the wrong products were rung up, or when a payment was reversed. Unlike deletion, cancellation preserves the receipt for historical records and audit trail. For issued receipts, cancellation on the physical fiscal device must be performed separately in accordance with ANAF regulations. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the receipt to cancel | ## Request Body (Optional) | Field | Type | Required | Description | |-------|------|----------|-------------| | `cancellationReason` | string | No | Reason for cancellation | | `cancellationNotes` | string | No | Internal notes about the cancellation | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/cancel \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "cancellationReason": "Customer returned all items", "cancellationNotes": "Full refund issued to customer card" }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/cancel', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ cancellationReason: 'Customer returned all items', cancellationNotes: 'Full refund issued to customer card' }) }); const data = await response.json(); ``` ## Response Returns the updated receipt with `status = cancelled` and `cancelledAt` timestamp: ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "number": "BON-2026-042", "status": "cancelled", "issueDate": "2026-02-18", "currency": "RON", "subtotal": "210.92", "vatAmount": "40.08", "total": "251.00", "paymentMethod": "mixed", "cashPayment": "100.00", "cardPayment": "151.00", "otherPayment": "0.00", "cashRegisterName": "Casa 1 - Front Desk", "fiscalNumber": "AAAA123456", "cancellationReason": "Customer returned all items", "cancellationNotes": "Full refund issued to customer card", "issuedAt": "2026-02-18T10:15:00Z", "cancelledAt": "2026-02-18T11:00:00Z", "createdAt": "2026-02-18T10:14:00Z", "updatedAt": "2026-02-18T11:00:00Z" } ``` ## State Changes ### Status Transition - **Before:** Any status except `invoiced` or `cancelled` - **After:** `status = cancelled` ### Timestamp - Sets `cancelledAt` to current UTC timestamp (ISO 8601 format) - Updates `updatedAt` timestamp ### Restrictions Applied Once cancelled: - Cannot be converted to invoice - Cannot be issued (if was in draft) - Cannot be edited or deleted - Serves as historical record only ## Validation Rules Can cancel receipts in these statuses: - `draft` — Not yet printed - `issued` — Already printed and handed to customer Cannot cancel receipts in these statuses: - `invoiced` — Already converted to a formal invoice (use credit note instead) - `cancelled` — Already cancelled ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Receipt not found or doesn't belong to the company | | 409 | `conflict` | Receipt status prevents cancellation (already invoiced or cancelled) | | 422 | `validation_error` | Invalid request body | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict - Already Cancelled ```json { "error": { "code": "conflict", "message": "Receipt cannot be cancelled", "details": { "status": "cancelled", "reason": "Receipt is already cancelled", "cancelledAt": "2026-02-18T11:00:00Z" } } } ``` ### Status Conflict - Already Invoiced ```json { "error": { "code": "conflict", "message": "Receipt cannot be cancelled", "details": { "status": "invoiced", "reason": "Receipt has already been converted to an invoice. Use a credit note to reverse the invoice instead.", "convertedInvoiceId": "650e8400-e29b-41d4-a716-446655440222", "convertedInvoiceNumber": "FAC-2026-050" } } } ``` ## Fiscal Compliance Note Cancelling a receipt in the system does not automatically void the physical fiscal receipt. For **issued** receipts, you must also: 1. Issue a cancellation receipt on the physical fiscal cash register device according to ANAF regulations 2. Keep the cancellation receipt as part of the Z report records 3. Ensure the cancellation is reflected in the daily Z report For **draft** receipts that were never printed on the fiscal device, no additional fiscal action is required. ## Common Cancellation Reasons - **Customer returned items** — Customer brought back purchased goods - **Wrong items rung up** — Cashier entered incorrect products - **Payment reversed** — Card payment was declined or reversed after printing - **Duplicate receipt** — Same transaction printed twice by mistake - **Customer complaint** — Transaction disputed by customer - **System error** — POS system error caused incorrect receipt ## Recovery After Cancellation ### Undo Accidental Cancellation If the receipt was cancelled by mistake, use the `POST /api/v1/receipts/{uuid}/restore` endpoint to restore it back to `draft` status. ### Create Corrected Receipt If the original transaction data was wrong: 1. Cancel the incorrect receipt 2. Create a new receipt with the correct data (`POST /api/v1/receipts`) 3. Issue the new receipt ## Related Endpoints - [Restore receipt](/api-reference/receipts/restore) - Restore an accidentally cancelled receipt - [Convert to invoice](/api-reference/receipts/convert) - Convert an issued receipt to a formal invoice - [Get email history](/api-reference/receipts/email-history) - View previously sent emails --- ## Convert Receipt to Invoice > Convert a fiscal receipt into a formal invoice URL: https://docs.storno.ro/api-reference/receipts/convert # Convert Receipt to Invoice Converts a fiscal receipt into a formal, legally-binding invoice. This action creates a new invoice with all the receipt's data, marks the receipt as `invoiced`, and establishes a link between the two documents. This is used when a business customer (B2B) requests a formal tax invoice for a purchase they made at the point of sale. Once converted, the receipt cannot be modified and serves as a historical reference to the original transaction. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the receipt to convert | ## Request Body (Optional) | Field | Type | Required | Description | |-------|------|----------|-------------| | `invoiceSeriesId` | string | No | UUID of invoice series to use | | `issueDate` | string | No | Override invoice issue date (default: today) | | `dueDate` | string | No | Override invoice due date (default: today) | | `clientId` | string | No | Override the client UUID for the invoice | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/convert \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "invoiceSeriesId": "660e8400-e29b-41d4-a716-446655440000", "issueDate": "2026-02-18", "dueDate": "2026-03-18" }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/convert', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ invoiceSeriesId: '660e8400-e29b-41d4-a716-446655440000', issueDate: '2026-02-18', dueDate: '2026-03-18' }) }); const data = await response.json(); ``` ## Response Returns the newly created invoice along with the updated receipt status: ```json { "invoice": { "uuid": "650e8400-e29b-41d4-a716-446655440222", "number": "FAC-2026-050", "direction": "outgoing", "isCreditNote": false, "seriesId": "660e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "660e8400-e29b-41d4-a716-446655440000", "name": "FAC", "nextNumber": 51, "prefix": "FAC-", "year": 2026 }, "clientId": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "client": { "uuid": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "name": "Acme SRL", "registrationNumber": "RO12345678", "address": "Strada Principala, nr. 10", "email": "office@acme.ro" }, "status": "draft", "issueDate": "2026-02-18", "dueDate": "2026-03-18", "currency": "RON", "exchangeRate": 1.0, "invoiceTypeCode": "380", "receiptReference": "BON-2026-042", "receiptId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "lines": [ { "uuid": "h8i9j0k1-l2m3-4567-nopq-789012345678", "lineNumber": 1, "description": "Coffee - Espresso", "quantity": "2.00", "unitPrice": "12.61", "unitOfMeasure": "pcs", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "percentage": "9.00" }, "subtotal": "25.22", "vatAmount": "2.27", "total": "27.49" }, { "uuid": "i9j0k1l2-m3n4-5678-opqr-890123456789", "lineNumber": 2, "description": "Notebook A5", "quantity": "2.00", "unitPrice": "41.18", "unitOfMeasure": "pcs", "vatRateId": "360e8400-e29b-41d4-a716-446655440000", "vatRate": { "percentage": "19.00" }, "subtotal": "82.36", "vatAmount": "15.65", "total": "98.01" } ], "subtotal": "107.58", "vatAmount": "17.92", "total": "125.50", "anafStatus": null, "createdAt": "2026-02-18T10:20:00Z", "updatedAt": "2026-02-18T10:20:00Z" }, "receipt": { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "number": "BON-2026-042", "status": "invoiced", "convertedInvoiceId": "650e8400-e29b-41d4-a716-446655440222", "convertedInvoiceNumber": "FAC-2026-050", "updatedAt": "2026-02-18T10:20:00Z" } } ``` ## State Changes ### Receipt Changes - **Status:** Changed from `issued` → `invoiced` - **convertedInvoiceId:** Set to the UUID of the created invoice - **convertedInvoiceNumber:** Set to the invoice number for easy reference ### Invoice Creation - New invoice created with status `draft` - All line items copied from the receipt - Client information copied from the receipt's `clientId` or matched via `customerCif` - `receiptId` and `receiptReference` fields set to link back to the receipt - Ready to be issued and uploaded to ANAF ## Validation Rules ### Receipt Status Can convert receipts in these statuses: - `issued` — Standard status for conversion Cannot convert receipts in these statuses: - `draft` — Receipt has not yet been printed and issued - `cancelled` — Transaction was voided - `invoiced` — Already converted to an invoice ### Client Requirement - To convert a receipt to an invoice, a client must be identifiable: - The receipt must have a linked `clientId`, OR - A `clientId` must be provided in the request body - If neither is present, the conversion will fail with a validation error ### Series Selection - If `invoiceSeriesId` is not provided, the default invoice series for the company is used - Series must be configured for outgoing invoices - Series must belong to the same company ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Receipt not found or doesn't belong to the company | | 409 | `conflict` | Receipt status prevents conversion or already invoiced | | 422 | `validation_error` | No client linked to the receipt; provide clientId in the request | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Already Invoiced ```json { "error": { "code": "conflict", "message": "Receipt cannot be converted", "details": { "status": "invoiced", "reason": "Receipt has already been converted to an invoice", "convertedInvoiceId": "650e8400-e29b-41d4-a716-446655440222", "convertedInvoiceNumber": "FAC-2026-050" } } } ``` ### No Client ```json { "error": { "code": "validation_error", "message": "Cannot convert receipt to invoice", "details": { "clientId": ["A client is required to generate an invoice. Provide a clientId in the request body or link a client to the receipt first."] } } } ``` ## Workflow Integration ### Customer Requests Invoice at POS 1. Issue receipt (`POST /api/v1/receipts/{uuid}/issue`) 2. Customer requests a formal invoice 3. **Convert to invoice** (`POST /api/v1/receipts/{uuid}/convert`) ← You are here 4. Upload invoice to ANAF (`POST /api/v1/invoices/{uuid}/submit`) 5. Email invoice to customer ### Post-Purchase Invoice Request 1. Receipt was issued earlier in the day 2. Customer contacts you later requesting an invoice 3. Link client to receipt if not already done 4. **Convert to invoice** with desired invoice date ## Post-Conversion Actions After conversion: 1. **Upload to ANAF** — Submit the invoice to ANAF e-Factura if required 2. **Generate PDF** — Create PDF version for the customer 3. **Send to customer** — Email invoice to the customer 4. **Archive receipt** — Keep receipt as reference document ## Best Practices 1. **Convert on the same day** — Issue the invoice on the same date as the receipt to avoid tax period discrepancies 2. **Link client before converting** — Ensure the receipt has a `clientId` or provide one at conversion time for clean data 3. **Use correct invoice series** — Select the appropriate series for B2B invoices 4. **Upload to ANAF promptly** — Submit the resulting invoice within the required ANAF timeframe --- ## Create Receipt > Create a new fiscal receipt (bon fiscal) URL: https://docs.storno.ro/api-reference/receipts/create # Create Receipt Creates a new fiscal receipt (bon fiscal) in draft status. Receipts document point-of-sale transactions and can later be converted into formal invoices when customers request a tax document. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | | `Idempotency-Key` | string | No | Client-generated unique key. Repeat submissions with the same key return the originally created receipt instead of creating a duplicate. Equivalent to passing `idempotencyKey` in the body. | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `documentSeriesId` | string | No | UUID of the receipt document series. If not provided, the default `receipt` series for the company is auto-assigned | | `issueDate` | string | Yes | Date of issue (YYYY-MM-DD) | | `currency` | string | Yes | Currency code (RON, EUR, USD, etc.) | | `cashRegisterName` | string | Yes | Name or label of the cash register | | `fiscalNumber` | string | Yes | Fiscal serial number of the cash register | | `paymentMethod` | string | Yes | Payment method: `cash`, `card`, `mixed`, `other` | | `cashPayment` | number | No | Amount paid in cash (default: 0) | | `cardPayment` | number | No | Amount paid by card (default: 0) | | `otherPayment` | number | No | Amount paid by other method (default: 0) | | `customerName` | string | No | Customer name (for B2B receipts) | | `customerCif` | string | No | Customer CIF/CUI (for B2B receipts) | | `clientId` | string | No | UUID of a linked Client object | | `notes` | string | No | Public notes on the receipt | | `internalNote` | string | No | Internal note (not printed on receipt) | | `idempotencyKey` | string | No | Client-generated unique key for retry-safe submissions. Same effect as the `Idempotency-Key` header; the header takes precedence if both are sent. Up to 255 chars. | | `lines` | array | Yes | Array of line items (minimum 1 item) | ### Line Item Object | Field | Type | Required | Description | |-------|------|----------|-------------| | `description` | string | Yes | Item description | | `quantity` | number | Yes | Quantity sold (must be > 0) | | `unitPrice` | number | Yes | Price per unit (excluding VAT) | | `vatRateId` | string | Yes | UUID of the VAT rate | | `unitOfMeasure` | string | No | Unit of measure (e.g., pcs, kg, l) | | `productId` | string | No | UUID of related product | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/receipts \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "issueDate": "2026-02-18", "currency": "RON", "cashRegisterName": "Casa 1 - Front Desk", "fiscalNumber": "AAAA123456", "paymentMethod": "mixed", "cashPayment": 100.00, "cardPayment": 151.00, "otherPayment": 0.00, "customerName": "Acme SRL", "customerCif": "RO12345678", "clientId": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "notes": "Thank you for your purchase!", "internalNote": "Loyalty card customer", "lines": [ { "description": "Coffee - Espresso", "quantity": 2, "unitPrice": 12.61, "unitOfMeasure": "pcs", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "d4e5f6a7-b8c9-0123-defa-234567890123" }, { "description": "Notebook A5", "quantity": 2, "unitPrice": 41.18, "unitOfMeasure": "pcs", "vatRateId": "360e8400-e29b-41d4-a716-446655440000", "productId": "f6a7b8c9-d0e1-2345-fabc-456789012345" } ] }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/receipts', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ documentSeriesId: '850e8400-e29b-41d4-a716-446655440000', issueDate: '2026-02-18', currency: 'RON', cashRegisterName: 'Casa 1 - Front Desk', fiscalNumber: 'AAAA123456', paymentMethod: 'mixed', cashPayment: 100.00, cardPayment: 151.00, otherPayment: 0.00, customerName: 'Acme SRL', customerCif: 'RO12345678', clientId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901', notes: 'Thank you for your purchase!', internalNote: 'Loyalty card customer', lines: [ { description: 'Coffee - Espresso', quantity: 2, unitPrice: 12.61, unitOfMeasure: 'pcs', vatRateId: '350e8400-e29b-41d4-a716-446655440000', productId: 'd4e5f6a7-b8c9-0123-defa-234567890123' }, { description: 'Notebook A5', quantity: 2, unitPrice: 41.18, unitOfMeasure: 'pcs', vatRateId: '360e8400-e29b-41d4-a716-446655440000', productId: 'f6a7b8c9-d0e1-2345-fabc-456789012345' } ] }) }); const data = await response.json(); ``` ## Response ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "number": "BON-2026-042", "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "BON", "nextNumber": 43, "prefix": "BON-", "year": 2026 }, "clientId": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "client": { "uuid": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "name": "Acme SRL", "registrationNumber": "RO12345678", "address": "Strada Principala, nr. 10", "email": "office@acme.ro" }, "status": "draft", "issueDate": "2026-02-18", "currency": "RON", "cashRegisterName": "Casa 1 - Front Desk", "fiscalNumber": "AAAA123456", "paymentMethod": "mixed", "cashPayment": "100.00", "cardPayment": "151.00", "otherPayment": "0.00", "customerName": "Acme SRL", "customerCif": "RO12345678", "notes": "Thank you for your purchase!", "internalNote": "Loyalty card customer", "lines": [ { "uuid": "c3d4e5f6-a7b8-9012-cdef-123456789012", "lineNumber": 1, "description": "Coffee - Espresso", "quantity": "2.00", "unitPrice": "12.61", "unitOfMeasure": "pcs", "productId": "d4e5f6a7-b8c9-0123-defa-234567890123", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "VAT 9%", "percentage": "9.00" }, "subtotal": "25.22", "vatAmount": "2.27", "total": "27.49" }, { "uuid": "e5f6a7b8-c9d0-1234-efab-345678901234", "lineNumber": 2, "description": "Notebook A5", "quantity": "2.00", "unitPrice": "41.18", "unitOfMeasure": "pcs", "productId": "f6a7b8c9-d0e1-2345-fabc-456789012345", "vatRateId": "360e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "360e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "subtotal": "82.36", "vatAmount": "15.65", "total": "98.01" } ], "subtotal": "107.58", "vatAmount": "17.92", "total": "125.50", "issuedAt": null, "cancelledAt": null, "convertedInvoiceId": null, "createdAt": "2026-02-18T10:14:00Z", "updatedAt": "2026-02-18T10:14:00Z" } ``` ## Validation Rules ### Payment Amounts - The sum of `cashPayment + cardPayment + otherPayment` must equal `total` - Individual payment amounts must be >= 0 - `paymentMethod` must match the active payment amounts: - `cash` — only `cashPayment > 0` - `card` — only `cardPayment > 0` - `other` — only `otherPayment > 0` - `mixed` — more than one payment type is > 0 ### Date - `issueDate` must be a valid date in YYYY-MM-DD format ### Currency - Must be a valid 3-letter currency code (ISO 4217) ### Line Items - Minimum 1 line item required - `quantity` must be greater than 0 - `unitPrice` must be greater than or equal to 0 - `vatRateId` must reference an existing VAT rate - If `productId` is provided, it must reference an existing product ### References - If `documentSeriesId` is provided, it must reference an existing series configured for `receipt` type; if omitted, the company's default `receipt` series is auto-assigned - If `clientId` is provided, it must reference an existing client ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid request body structure | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Referenced entity not found (series, VAT rate, product, client) | | 422 | `validation_error` | Validation failed (see error details for specific field errors) | | 500 | `internal_error` | Server error occurred | ## Example Validation Error Response ```json { "error": { "code": "validation_error", "message": "Validation failed", "details": { "paymentMethod": ["Sum of cash, card, and other payments must equal total"], "lines.0.quantity": ["Quantity must be greater than 0"], "lines.1.vatRateId": ["VAT rate not found"] } } } ``` ## Common Scenarios ### Cash-Only Purchase ```javascript { paymentMethod: 'cash', cashPayment: 50.00, cardPayment: 0.00, otherPayment: 0.00 } ``` ### Card-Only Purchase ```javascript { paymentMethod: 'card', cashPayment: 0.00, cardPayment: 150.00, otherPayment: 0.00 } ``` ### Meal Ticket + Cash ```javascript { paymentMethod: 'mixed', cashPayment: 10.00, cardPayment: 0.00, otherPayment: 20.00 // meal ticket value } ``` ### B2B Receipt Without Client Link ```javascript { customerName: 'Firm Anonima SRL', customerCif: 'RO99887766' // no clientId — customer data recorded directly } ``` ### B2B Receipt With Client Link ```javascript { clientId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901', customerName: 'Acme SRL', customerCif: 'RO12345678' // linked to existing Client — enables clean invoice conversion } ``` ## Idempotency Send a unique `Idempotency-Key` header (or `idempotencyKey` body field) when retrying receipt creation in scenarios where the original response was lost — for example, a network timeout mid-request or a queued offline POS sale. The backend records the key on the receipt; subsequent submissions with the same key return the originally-created receipt instead of inserting a duplicate. The key is a free-form string up to 255 characters; UUIDs are recommended. Keys are unique across the entire `receipt` table, so use a high-entropy generator. ```bash {% title="Idempotent retry" %} curl -X POST https://api.storno.ro/api/v1/receipts \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Idempotency-Key: pos-1761417321-7k3xq2v9" \ -H "Content-Type: application/json" \ -d '{ ... receipt body ... }' ``` ## Next Steps After creating a receipt: 1. Mark as issued when the receipt is printed and handed to the customer (`POST /api/v1/receipts/{uuid}/issue`) 2. If the customer requests an invoice, convert to invoice (`POST /api/v1/receipts/{uuid}/convert`) --- ## Delete Receipt > Permanently delete a receipt (draft status only) URL: https://docs.storno.ro/api-reference/receipts/delete # Delete Receipt Permanently deletes a receipt and all its line items. Only receipts in `draft` status can be deleted. Once issued, invoiced, or cancelled, a receipt cannot be deleted. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the receipt to delete | ## Request ```bash {% title="cURL" %} curl -X DELETE https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890', { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); // Success: 204 No Content (no response body) if (response.status === 204) { console.log('Receipt deleted successfully'); } ``` ## Response **Success:** Returns `204 No Content` with an empty response body. The receipt and all associated line items are permanently deleted from the database. ## Restrictions ### Status Requirement Only receipts with `status = draft` can be deleted. Receipts in the following states **cannot** be deleted: - `issued` - Already printed and handed to customer - `invoiced` - Converted to a formal invoice - `cancelled` - Already cancelled For issued or invoiced receipts, use the [cancel endpoint](/api-reference/receipts/cancel) instead. ### Referential Integrity - Deleting a receipt does not roll back the series number counter - If the receipt was converted to an invoice, the invoice is **not** deleted ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Receipt not found or doesn't belong to the company | | 409 | `conflict` | Receipt status prevents deletion (not in draft) | | 500 | `internal_error` | Server error occurred | ## Example Error Responses ### Status Conflict - Issued ```json { "error": { "code": "conflict", "message": "Receipt cannot be deleted", "details": { "status": "issued", "reason": "Only draft receipts can be deleted. Use cancel instead.", "issuedAt": "2026-02-18T10:15:00Z" } } } ``` ### Not Found ```json { "error": { "code": "not_found", "message": "Receipt not found" } } ``` ## Delete vs Cancel ### Delete - **Use when:** Receipt was created by mistake and is still in draft - **Preserves:** Nothing — permanently removed - **Can be done:** Only in `draft` status - **Audit trail:** None ### Cancel - **Use when:** Receipt was issued but must be voided (requires a cancellation receipt on the fiscal device) - **Preserves:** Full audit trail and historical data - **Can be done:** At `draft` or `issued` status - **Audit trail:** Full ## Best Practices 1. **Delete only drafts** - Never attempt to delete an issued receipt 2. **Verify before deleting** - Double-check you are deleting the correct document 3. **Cancel issued receipts** - If a receipt was already printed, use cancel to maintain the audit trail 4. **Fiscal compliance** - Deleting a draft that was never printed on the fiscal device has no fiscal impact; cancelling an issued receipt requires a corresponding cancellation receipt on the physical device --- ## Download Receipt PDF > Download the PDF representation of a receipt URL: https://docs.storno.ro/api-reference/receipts/pdf # Download Receipt PDF Downloads the PDF for a fiscal receipt. The PDF is generated using the company's configured PDF template and includes all line items, payment breakdown, customer details, and cash register information. ``` GET /api/v1/receipts/{uuid}/pdf ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token or API key for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | UUID of the receipt | ## Request ```bash {% title="cURL" %} curl https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/pdf \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -o receipt.pdf ``` ```javascript {% title="JavaScript" %} const response = await fetch( 'https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/pdf', { headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } } ); const blob = await response.blob(); ``` ## Response Returns the PDF file as binary data with appropriate headers. **Headers:** | Header | Value | |--------|-------| | `Content-Type` | `application/pdf` | | `Content-Disposition` | `attachment; filename="bon-{number}.pdf"` | ## Error Codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | No access to the specified company | | `404` | Receipt not found | | `500` | PDF generation failed | --- ## Get Email Defaults > Get pre-filled email content for a receipt URL: https://docs.storno.ro/api-reference/receipts/email-defaults # Get Email Defaults Returns pre-filled email content for a receipt based on the company's configured email template. All template variables are already substituted with real values from the receipt and customer records. Use this to populate the email form before sending. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the receipt | ## Request ```bash {% title="cURL" %} curl https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/email-defaults \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/email-defaults', { headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const defaults = await response.json(); // Use to pre-populate email form document.getElementById('email-to').value = defaults.to; document.getElementById('email-subject').value = defaults.subject; document.getElementById('email-body').value = defaults.body; ``` ## Response Returns an object with pre-filled `to`, `subject`, and `body` fields: ```json { "to": "office@acme.ro", "subject": "Bon fiscal BON-2026-042 de la Your Company SRL", "body": "Buna ziua,\n\nGasiti atasat bonul fiscal BON-2026-042 in valoare de 251.00 RON.\n\nMultumim pentru achizitie!\n\nYour Company SRL" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `to` | string | Customer email address (from linked client or `customerName` context) | | `subject` | string | Pre-filled subject with all template variables replaced | | `body` | string | Pre-filled body with all template variables replaced | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Receipt not found or doesn't belong to the company | ## Related Endpoints - [Send email](/api-reference/receipts/email) - Send the email using these defaults - [Get email history](/api-reference/receipts/email-history) - View previously sent emails --- ## Get Email History > Get email sending history for a receipt URL: https://docs.storno.ro/api-reference/receipts/email-history # Get Email History Returns the complete history of emails sent for a specific receipt, ordered by sent date (newest first). ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the receipt | ## Request ```bash {% title="cURL" %} curl https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/emails \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/emails', { headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const emailHistory = await response.json(); emailHistory.forEach(entry => { console.log(`Sent to ${entry.to} on ${entry.sentAt}: ${entry.deliveryStatus}`); }); ``` ## Response Returns an array of email log entries: ```json [ { "id": "8f9a0b1c-2d3e-4f5a-6b7c-8d9e0f1a2b3c", "to": "office@acme.ro", "cc": ["accounting@acme.ro"], "bcc": null, "subject": "Bon fiscal BON-2026-042", "attachments": ["bon-BON-2026-042.pdf"], "sentAt": "2026-02-18T10:30:00Z", "deliveryStatus": "delivered", "openedAt": "2026-02-18T10:45:00Z", "bouncedAt": null, "bounceReason": null, "sentBy": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "John Doe", "email": "john@yourcompany.ro" } }, { "id": "7e8f9a0b-1c2d-3e4f-5a6b-7c8d9e0f1a2b", "to": "old@acme.ro", "cc": null, "bcc": null, "subject": "Bon fiscal BON-2026-042", "attachments": ["bon-BON-2026-042.pdf"], "sentAt": "2026-02-18T10:16:00Z", "deliveryStatus": "bounced", "openedAt": null, "bouncedAt": "2026-02-18T10:17:00Z", "bounceReason": "Mailbox does not exist", "sentBy": { "id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b", "name": "John Doe", "email": "john@yourcompany.ro" } } ] ``` ### Email Record Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Email log UUID | | `to` | string | Primary recipient email address | | `cc` | string[]\|null | CC recipients | | `bcc` | string[]\|null | BCC recipients | | `subject` | string | Email subject line | | `attachments` | string[] | List of attached file names | | `sentAt` | string | ISO 8601 timestamp when email was sent | | `deliveryStatus` | string | Current delivery status (see values below) | | `openedAt` | string\|null | When the email was first opened | | `bouncedAt` | string\|null | When the email bounced | | `bounceReason` | string\|null | Reason for bounce | | `sentBy` | object | User who triggered the send | ### Delivery Status Values | Status | Description | |--------|-------------| | `queued` | Email is queued for sending | | `sent` | Sent to recipient's mail server | | `delivered` | Delivered to recipient's inbox | | `opened` | Email was opened by recipient | | `bounced` | Email bounced (hard or soft) | | `failed` | Email sending failed | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Receipt not found or doesn't belong to the company | ## Related Endpoints - [Send email](/api-reference/receipts/email) - Send a new email - [Get email defaults](/api-reference/receipts/email-defaults) - Get pre-filled email content --- ## Get Receipt > Retrieve detailed information for a specific receipt including line items URL: https://docs.storno.ro/api-reference/receipts/get # Get Receipt Retrieves complete details for a specific fiscal receipt, including all line items, payment breakdown, customer information, and calculated totals. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the receipt to retrieve | ## Request ```bash {% title="cURL" %} curl -X GET https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "number": "BON-2026-042", "seriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "BON", "nextNumber": 43, "prefix": "BON-", "year": 2026 }, "status": "issued", "issueDate": "2026-02-18", "currency": "RON", "subtotal": "210.92", "vatAmount": "40.08", "total": "251.00", "paymentMethod": "mixed", "cashPayment": "100.00", "cardPayment": "151.00", "otherPayment": "0.00", "cashRegisterName": "Casa 1 - Front Desk", "fiscalNumber": "AAAA123456", "customerName": "Acme SRL", "customerCif": "RO12345678", "clientId": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "client": { "uuid": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "name": "Acme SRL", "registrationNumber": "RO12345678", "address": "Strada Principala, nr. 10", "city": "Cluj-Napoca", "county": "Cluj", "country": "RO", "postalCode": "400000", "email": "office@acme.ro", "phone": "+40721234567" }, "notes": "Thank you for your purchase!", "internalNote": "Loyalty card customer", "lines": [ { "uuid": "c3d4e5f6-a7b8-9012-cdef-123456789012", "lineNumber": 1, "description": "Coffee - Espresso", "quantity": "2.00", "unitPrice": "12.61", "unitOfMeasure": "pcs", "productId": "d4e5f6a7-b8c9-0123-defa-234567890123", "product": { "uuid": "d4e5f6a7-b8c9-0123-defa-234567890123", "name": "Coffee - Espresso", "code": "COF-ESP", "unitOfMeasure": "pcs" }, "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "350e8400-e29b-41d4-a716-446655440000", "name": "VAT 9%", "percentage": "9.00" }, "subtotal": "25.22", "vatAmount": "2.27", "total": "27.49" }, { "uuid": "e5f6a7b8-c9d0-1234-efab-345678901234", "lineNumber": 2, "description": "Notebook A5", "quantity": "2.00", "unitPrice": "41.18", "unitOfMeasure": "pcs", "productId": "f6a7b8c9-d0e1-2345-fabc-456789012345", "product": { "uuid": "f6a7b8c9-d0e1-2345-fabc-456789012345", "name": "Notebook A5", "code": "NOTE-A5", "unitOfMeasure": "pcs" }, "vatRateId": "360e8400-e29b-41d4-a716-446655440000", "vatRate": { "uuid": "360e8400-e29b-41d4-a716-446655440000", "name": "Standard VAT", "percentage": "19.00" }, "subtotal": "82.36", "vatAmount": "15.65", "total": "98.01" } ], "convertedInvoiceId": null, "convertedInvoiceNumber": null, "issuedAt": "2026-02-18T10:15:00Z", "cancelledAt": null, "createdAt": "2026-02-18T10:14:00Z", "updatedAt": "2026-02-18T10:15:00Z" } ``` ## Response Fields ### Core Information | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `number` | string | Receipt number | | `status` | string | Current status (draft/issued/invoiced/cancelled) | | `series` | object | Series information with prefix and year | | `client` | object \| null | Linked client details (if associated) | ### Payment Information | Field | Type | Description | |-------|------|-------------| | `paymentMethod` | string | Overall payment method: `cash`, `card`, `mixed`, `other` | | `cashPayment` | string | Amount paid in cash | | `cardPayment` | string | Amount paid by card | | `otherPayment` | string | Amount paid by other method (voucher, meal ticket, etc.) | ### Cash Register Information | Field | Type | Description | |-------|------|-------------| | `cashRegisterName` | string | User-defined label for the cash register | | `fiscalNumber` | string | Official fiscal serial number from ANAF registration | ### Customer Information | Field | Type | Description | |-------|------|-------------| | `customerName` | string \| null | Customer name (for B2B receipts) | | `customerCif` | string \| null | Customer CIF/CUI (for B2B receipts) | ### Financial Information | Field | Type | Description | |-------|------|-------------| | `currency` | string | Currency code (e.g., RON, EUR) | | `subtotal` | string | Subtotal before VAT | | `vatAmount` | string | Total VAT amount | | `total` | string | Grand total including VAT | | `lines` | array | Array of line items with products and pricing | ### Dates and Status | Field | Type | Description | |-------|------|-------------| | `issueDate` | string | Date when receipt was issued | | `issuedAt` | string \| null | Timestamp when issued | | `cancelledAt` | string \| null | Timestamp when cancelled | | `convertedInvoiceId` | string \| null | UUID of created invoice (if invoiced) | | `convertedInvoiceNumber` | string \| null | Number of created invoice (if invoiced) | ### Additional Information | Field | Type | Description | |-------|------|-------------| | `notes` | string \| null | Public notes about the purchase | | `internalNote` | string \| null | Internal note (not visible on printed receipt) | ### Line Item Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Line item unique identifier | | `lineNumber` | integer | Sequential line number | | `description` | string | Item description | | `quantity` | string | Quantity sold (decimal string) | | `unitPrice` | string | Price per unit (excluding VAT) | | `unitOfMeasure` | string | Unit of measure (e.g., pcs, kg, l) | | `product` | object \| null | Related product details | | `vatRate` | object | VAT rate details with percentage | | `subtotal` | string | Line subtotal (before VAT) | | `vatAmount` | string | Line VAT amount | | `total` | string | Line total (including VAT) | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Receipt not found or doesn't belong to the company | | 500 | `internal_error` | Server error occurred | ## B2B Receipts and Client Linking When a business customer purchases goods and needs a tax document, the receipt can be associated with a Client object in two ways: 1. **Via customerName / customerCif** — The customer's company data is recorded directly on the receipt without linking to an existing Client record 2. **Via clientId** — The receipt is linked to an existing Client object in the system, enabling conversion to a formal invoice Both approaches are valid. Linking to a Client object is recommended if the receipt will be converted to an invoice, as it pre-fills all required client data. ## Conversion to Invoice When the receipt is converted to an invoice: - `status` changes to `invoiced` - `convertedInvoiceId` is set to the new invoice UUID - `convertedInvoiceNumber` is set to the new invoice number - Receipt data is copied to the invoice - Receipt reference is added to the invoice notes --- ## Issue Receipt > Mark a receipt as issued when it is printed on the fiscal cash register URL: https://docs.storno.ro/api-reference/receipts/issue # Issue Receipt Marks a receipt as issued, recording the moment the fiscal receipt is printed and handed to the customer. This action transitions the receipt from `draft` status to `issued` status and records the timestamp. When the receipt is issued, the next sequential number from its assigned `receipt` series is permanently assigned. If no series was explicitly set on the receipt, the company's default `receipt` series is auto-found and used at this point. Once issued, the receipt becomes immutable and can only be cancelled or converted to an invoice. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the receipt to mark as issued | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/issue \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/issue', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response Returns the updated receipt with `status = issued` and `issuedAt` timestamp: ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "number": "BON-2026-042", "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "BON", "nextNumber": 43 }, "status": "issued", "issueDate": "2026-02-18", "currency": "RON", "subtotal": "210.92", "vatAmount": "40.08", "total": "251.00", "paymentMethod": "mixed", "cashPayment": "100.00", "cardPayment": "151.00", "otherPayment": "0.00", "cashRegisterName": "Casa 1 - Front Desk", "fiscalNumber": "AAAA123456", "customerName": "Acme SRL", "customerCif": "RO12345678", "issuedAt": "2026-02-18T10:15:00Z", "cancelledAt": null, "convertedInvoiceId": null, "createdAt": "2026-02-18T10:14:00Z", "updatedAt": "2026-02-18T10:15:00Z" } ``` ## State Changes ### Status Transition - **Before:** `status = draft` - **After:** `status = issued` ### Series & Number Assignment - The next sequential number from the assigned `receipt` series is permanently locked in - If no `documentSeriesId` was set on the receipt, the company's default `receipt` series is auto-found and assigned at this point - Once issued, the series and document number cannot be changed ### Timestamp - Sets `issuedAt` to current UTC timestamp (ISO 8601 format) - Updates `updatedAt` timestamp ### Restrictions Applied Once marked as issued: - Cannot be updated (PUT requests will fail) - Cannot be deleted (DELETE requests will fail) - Can be cancelled or converted to an invoice ## Validation Rules - Receipt must have `status = draft` - Cannot issue a receipt that is already issued, invoiced, or cancelled - Receipt must have at least one line item ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Receipt not found or doesn't belong to the company | | 409 | `conflict` | Receipt status prevents issuing (not in draft status) | | 422 | `validation_error` | Receipt data is incomplete or invalid | | 500 | `internal_error` | Server error occurred | ## Example Error Response ### Status Conflict ```json { "error": { "code": "conflict", "message": "Receipt cannot be issued", "details": { "status": "issued", "reason": "Receipt is already marked as issued", "issuedAt": "2026-02-18T10:15:00Z" } } } ``` ## Workflow Integration ### Standard POS Flow 1. Create receipt draft (`POST /api/v1/receipts`) 2. Print receipt on fiscal cash register device 3. **Mark as issued** (`POST /api/v1/receipts/{uuid}/issue`) ← You are here 4. Hand receipt to customer 5. If customer requests invoice, convert (`POST /api/v1/receipts/{uuid}/convert`) ## Reversibility There is no "unissue" action. Once issued, the receipt remains in `issued` status until: - You cancel it (→ `cancelled`) - You convert it to an invoice (→ `invoiced`) If issued by mistake: 1. Cannot revert to draft 2. Must cancel if needed (requires cancellation receipt on the fiscal device) 3. Create a new receipt if the transaction was entered incorrectly ## Related Endpoints - [Cancel receipt](/api-reference/receipts/cancel) - Cancel an issued receipt - [Convert receipt to invoice](/api-reference/receipts/convert) - Convert to a formal invoice - [Update receipt](/api-reference/receipts/update) - Edit the draft before issuing --- ## List Receipts > Retrieve a paginated list of receipts with optional filtering URL: https://docs.storno.ro/api-reference/receipts/list # List Receipts Retrieves a paginated list of fiscal receipts (bonuri fiscale) for the authenticated company. Receipts document point-of-sale transactions and can optionally be converted into invoices when customers request a formal tax document. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Query Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `page` | integer | No | Page number (default: 1) | | `limit` | integer | No | Items per page (default: 20, max: 100) | | `search` | string | No | Search term for receipt number, customer name, or fiscal number | | `status` | string | No | Filter by status: `draft`, `issued`, `invoiced`, `cancelled` | | `from` | string | No | Start date filter (ISO 8601 format: YYYY-MM-DD) | | `to` | string | No | End date filter (ISO 8601 format: YYYY-MM-DD) | | `paymentMethod` | string | No | Filter by payment method: `cash`, `card`, `mixed`, `other` | | `cashRegisterName` | string | No | Filter by cash register name | ## Request ```bash {% title="cURL" %} curl -X GET "https://api.storno.ro/api/v1/receipts?page=1&limit=20&status=issued" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/receipts?page=1&limit=20&status=issued', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response ```json { "data": [ { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "number": "BON-2026-042", "seriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "BON", "nextNumber": 43, "prefix": "BON-", "year": 2026 }, "status": "issued", "issueDate": "2026-02-18", "currency": "RON", "subtotal": "210.92", "vatAmount": "40.08", "total": "251.00", "paymentMethod": "mixed", "cashPayment": "100.00", "cardPayment": "151.00", "otherPayment": "0.00", "cashRegisterName": "Casa 1 - Front Desk", "fiscalNumber": "AAAA123456", "customerName": "Acme SRL", "customerCif": "RO12345678", "issuedAt": "2026-02-18T10:15:00Z", "convertedInvoiceId": null, "createdAt": "2026-02-18T10:14:00Z", "updatedAt": "2026-02-18T10:15:00Z" } ], "total": 42, "page": 1, "limit": 20, "pages": 3 } ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `data` | array | Array of receipt objects | | `total` | integer | Total number of receipts matching the filters | | `page` | integer | Current page number | | `limit` | integer | Items per page | | `pages` | integer | Total number of pages | ### Receipt Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `number` | string | Receipt number (e.g., BON-2026-042) | | `status` | string | Status: `draft`, `issued`, `invoiced`, `cancelled` | | `issueDate` | string | Date of issue (YYYY-MM-DD) | | `currency` | string | Currency code | | `subtotal` | string | Subtotal amount (excluding VAT) | | `vatAmount` | string | Total VAT amount | | `total` | string | Total amount (including VAT) | | `paymentMethod` | string | Payment method: `cash`, `card`, `mixed`, `other` | | `cashPayment` | string | Amount paid in cash | | `cardPayment` | string | Amount paid by card | | `otherPayment` | string | Amount paid by other method | | `cashRegisterName` | string | Name or label of the cash register | | `fiscalNumber` | string | Fiscal serial number of the cash register | | `customerName` | string \| null | Customer name (for B2B receipts) | | `customerCif` | string \| null | Customer CIF/CUI (for B2B receipts) | | `series` | object | Series details | | `issuedAt` | string \| null | ISO 8601 timestamp when issued | | `convertedInvoiceId` | string \| null | UUID of the created invoice (if invoiced) | ## Status Lifecycle Receipts follow this status flow: - **draft** — Initial state when created - **issued** — Receipt printed and handed to customer - **invoiced** — Customer requested an invoice; receipt was converted - **cancelled** — Receipt was voided Once a receipt is `invoiced` or `cancelled`, it cannot be modified. ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 422 | `validation_error` | Invalid query parameters (e.g., invalid status value, invalid date format) | | 500 | `internal_error` | Server error occurred | ## Use Cases ### Daily Sales Summary Retrieve all receipts issued on a specific day: ``` GET /api/v1/receipts?from=2026-02-18&to=2026-02-18&status=issued ``` ### Cash Register Audit List all receipts for a specific register: ``` GET /api/v1/receipts?cashRegisterName=Casa%201&from=2026-02-01&to=2026-02-28 ``` ### Pending Invoice Conversions Find all issued receipts that have not yet been converted: ``` GET /api/v1/receipts?status=issued ``` ## Best Practices 1. **Filter by date range** - Always use `from`/`to` filters for daily reporting to keep response sizes manageable 2. **Monitor invoiced status** - Track which receipts have been converted to invoices for VAT reporting 3. **Audit by register** - Use `cashRegisterName` filter for per-register reconciliation 4. **Regular reconciliation** - Compare receipt totals against POS system daily totals --- ## Refund Receipt > Issue a counter-receipt that mirrors all lines as negative amounts. URL: https://docs.storno.ro/api-reference/receipts/refund # Refund Receipt Creates a refund (counter-)receipt that mirrors the parent receipt's lines with negative quantities and inverted payment amounts. The refund is automatically issued and linked back to the parent via `refundOf`. ```http POST /api/v1/receipts/{uuid}/refund ``` Use this when a customer returns goods or you need to undo a sale that's already been issued. The original receipt remains intact for audit; the refund records the money-out event in your cash-register ledger. ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | UUID of the receipt to refund | ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token | | `X-Company` | string | Yes | Company UUID | ## Body Empty body (or omitted) — full refund: every line is mirrored at full quantity. For a partial refund, send `lineSelections`: ```json { "lineSelections": [ { "sourceLineId": "c3d4e5f6-a7b8-...", "quantity": "1" }, { "sourceLineId": "e5f6a7b8-c9d0-...", "quantity": "0.5" } ] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `lineSelections[].sourceLineId` | string | Yes | UUID of a line on the parent receipt | | `lineSelections[].quantity` | string \| number | Yes | Positive quantity to refund. Must not exceed the line's remaining refundable quantity (parent line qty minus what's already been refunded across previous partial refunds for this parent). | A receipt can be partially refunded multiple times; each call carves off some of the remaining refundable quantity. Once everything has been refunded the next call returns `422` with "Receipt has already been refunded." for full-refund attempts, or "Requested quantity (X) exceeds remaining refundable quantity (Y) for line ..." for individual lines. **Cancelling a refund returns its quantities to the pool.** Refund receipts are auto-issued and can be cancelled like any other issued receipt via `POST /api/v1/receipts/{uuid}/cancel`. Once cancelled, the parent's refunded-quantity tally drops the cancelled refund's lines, so a fresh `/refund` call can re-refund those quantities. The parent's `refundedBy` JSON field also stops listing cancelled refunds (it's populated from active refunds only). When `lineSelections` is provided, the refund's `cashPayment` / `cardPayment` / `otherPayment` amounts are scaled proportionally to the refunded gross share so the cash-register ledger stays consistent. Per-line discounts are also scaled by the same fraction for the partial line. ## Preconditions - Parent receipt status must be `issued`. Drafts and cancelled receipts can't be refunded. - Parent must not itself be a refund (`refundOf` is null). - For a full refund (no `lineSelections`): the parent must not have any `refundedBy` entries. - For a partial refund: each requested `quantity` must be > 0 and ≤ the source line's remaining refundable quantity. Violating any of these returns `422 Unprocessable Entity` with a Romanian-language explanation in `error`. ## Behaviour The new receipt: - Gets a fresh UUID and sequential number from the same document series as the parent. - Inherits company, client, customer fiscal info, currency, exchange rate, issuer, sales agent, notes, mentions, project reference, and cash-register identifiers from the parent. - Mirrors **every line** with `quantity = -original` and `discount = -original`. Unit prices, VAT rates, and units of measure are unchanged so totals come out correctly negative. - Mirrors `cashPayment`, `cardPayment`, and `otherPayment` as negative amounts. - Sets `paymentMethod` to the parent's value. - Auto-issues — status is `issued` on return, and `issuedAt` is stamped. ## Permissions Requires `invoice.refund`. ## Response `201 Created`. Returns the new refund receipt under the `receipt:detail` group. ```json { "id": "f1d3a4b5-...", "number": "BON-2026-099", "status": "issued", "total": "-125.50", "subtotal": "-105.46", "vatTotal": "-20.04", "currency": "RON", "issueDate": "2026-04-25", "issuedAt": "2026-04-25T18:14:33+03:00", "refundOf": { "id": "a1b2c3d4-...", "number": "BON-2026-042" }, "lines": [ { "description": "Coffee — Espresso", "quantity": "-2.0000", "unitPrice": "12.61", "vatRate": "9.00", "lineTotal": "-25.22", "vatAmount": "-2.27" } ] } ``` The parent receipt's `refundedBy` array is updated to include the new refund's id and number — clients should refetch the parent if they want to disable a "Refund" button there. The array reflects only **active** refunds; cancelled refund receipts are filtered out. ## Errors | Status | Description | |--------|-------------| | 401 | Unauthenticated | | 403 | Permission denied or wrong company | | 404 | Receipt not found | | 422 | Parent isn't issued / is itself a refund / has already been refunded | ## Related - [Cancel receipt](/api-reference/receipts/cancel) — for receipts that haven't been issued yet, or to void an issued one without producing an audit record - [Cash register ledger](/api-reference/cash-register/ledger) — refunds appear here as negative amounts --- ## Restore Receipt > Restore a cancelled receipt back to draft status URL: https://docs.storno.ro/api-reference/receipts/restore # Restore Receipt Restores a cancelled receipt back to `draft` status. Use this endpoint to undo an accidental cancellation. The receipt can then be edited and re-issued. Only receipts in `cancelled` status can be restored. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the receipt to restore | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/restore \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/restore', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here' } }); const data = await response.json(); ``` ## Response Returns the restored receipt with `status = draft` and `cancelledAt` cleared: ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "number": "BON-2026-042", "seriesId": "850e8400-e29b-41d4-a716-446655440000", "series": { "uuid": "850e8400-e29b-41d4-a716-446655440000", "name": "BON", "nextNumber": 43 }, "status": "draft", "issueDate": "2026-02-18", "currency": "RON", "subtotal": "210.92", "vatAmount": "40.08", "total": "251.00", "paymentMethod": "mixed", "cashPayment": "100.00", "cardPayment": "151.00", "otherPayment": "0.00", "cashRegisterName": "Casa 1 - Front Desk", "fiscalNumber": "AAAA123456", "cancellationReason": null, "cancellationNotes": null, "issuedAt": null, "cancelledAt": null, "createdAt": "2026-02-18T10:14:00Z", "updatedAt": "2026-02-18T11:30:00Z" } ``` ## State Changes - **Status:** Changed from `cancelled` → `draft` - **cancelledAt:** Cleared (set to `null`) - **cancellationReason:** Cleared (set to `null`) - **cancellationNotes:** Cleared (set to `null`) - **updatedAt:** Updated to current UTC timestamp After restoring, the receipt can be edited, re-issued, or deleted. ## Validation Rules - Receipt must be in `cancelled` status - Cannot restore a receipt that is `draft`, `issued`, or `invoiced` ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Receipt not found or doesn't belong to the company | | 422 | `validation_error` | Receipt is not in cancelled status | | 500 | `internal_error` | Server error occurred | ## Example Error Response ### Not Cancelled ```json { "error": { "code": "validation_error", "message": "Receipt cannot be restored", "details": { "status": "draft", "reason": "Only cancelled receipts can be restored" } } } ``` ## Fiscal Compliance Note Restoring a receipt in the system to `draft` does not automatically reverse any fiscal cancellation receipt that was issued on the physical cash register device. If a cancellation receipt was printed on the device, you must handle the fiscal correction separately in accordance with ANAF regulations. Restore is most appropriate for receipts that were cancelled in the system but where no physical cancellation receipt was printed yet. ## Workflow Integration ### Restore Flow 1. Identify accidentally cancelled receipt 2. **Call restore endpoint** (`POST /api/v1/receipts/{uuid}/restore`) ← You are here 3. Optionally edit the receipt if needed 4. Re-issue the receipt (`POST /api/v1/receipts/{uuid}/issue`) ## Related Endpoints - [Cancel receipt](/api-reference/receipts/cancel) - Cancel a receipt - [Issue receipt](/api-reference/receipts/issue) - Issue the restored draft - [Update receipt](/api-reference/receipts/update) - Edit the restored draft --- ## Send Receipt Email > Send a receipt to a customer via email with PDF attachment URL: https://docs.storno.ro/api-reference/receipts/email # Send Receipt Email Sends a receipt to a customer via email with the PDF attached. The subject and body can be customized or left blank to use the company's default email template. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the receipt to email | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `to` | string | Yes | Recipient email address | | `subject` | string | No | Email subject (uses default template if omitted) | | `body` | string | No | Email body (uses default template if omitted) | | `cc` | string[] | No | Array of CC email addresses | | `bcc` | string[] | No | Array of BCC email addresses | ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/email \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "to": "office@acme.ro", "subject": "Bon fiscal BON-2026-042", "body": "Buna ziua,\n\nGasiti atasat bonul fiscal BON-2026-042 in valoare de 251.00 RON.\n\nMultumim pentru achizitie!\n\nYour Company SRL", "cc": ["accounting@acme.ro"] }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890/email', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ to: 'office@acme.ro', subject: 'Bon fiscal BON-2026-042', body: 'Buna ziua,\n\nGasiti atasat bonul fiscal BON-2026-042 in valoare de 251.00 RON.\n\nMultumim pentru achizitie!\n\nYour Company SRL', cc: ['accounting@acme.ro'] }) }); const data = await response.json(); ``` ## Response Returns an email log object confirming the email was sent: ```json { "id": "8f9a0b1c-2d3e-4f5a-6b7c-8d9e0f1a2b3c", "to": "office@acme.ro", "cc": ["accounting@acme.ro"], "bcc": null, "subject": "Bon fiscal BON-2026-042", "attachments": ["bon-BON-2026-042.pdf"], "sentAt": "2026-02-18T10:30:00Z", "deliveryStatus": "sent" } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Email log UUID | | `to` | string | Primary recipient email address | | `cc` | string[]\|null | CC recipients | | `bcc` | string[]\|null | BCC recipients | | `subject` | string | Subject line that was sent | | `attachments` | string[] | List of attached file names | | `sentAt` | string | ISO 8601 timestamp of when the email was sent | | `deliveryStatus` | string | `sent`, `queued`, or `failed` | ## Template Variables When using the default email template (subject or body omitted), the following variables are substituted automatically: | Variable | Description | Example | |----------|-------------|---------| | `[[customer_name]]` | Customer name from receipt | Acme SRL | | `[[receipt_number]]` | Receipt number | BON-2026-042 | | `[[total]]` | Formatted total with currency | 251.00 RON | | `[[issue_date]]` | Issue date | 18.02.2026 | | `[[company_name]]` | Your company name | Your Company SRL | | `[[currency]]` | Currency code | RON | ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid or missing `to` email address | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Receipt not found or doesn't belong to the company | | 500 | `internal_error` | Email sending failed | ## Related Endpoints - [Get email defaults](/api-reference/receipts/email-defaults) - Get pre-filled subject and body - [Get email history](/api-reference/receipts/email-history) - View previously sent emails - [Download PDF](/api-reference/receipts/pdf) - Download the PDF directly --- ## Update Receipt > Update an existing receipt (draft status only) URL: https://docs.storno.ro/api-reference/receipts/update # Update Receipt Updates an existing fiscal receipt. Only receipts in `draft` status can be updated. Once issued, invoiced, or cancelled, a receipt becomes immutable. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Path Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the receipt to update | ## Request Body All fields from the create endpoint can be updated. The entire receipt is replaced with the new data. | Field | Type | Required | Description | |-------|------|----------|-------------| | `documentSeriesId` | string | No | UUID of the receipt document series. Can be changed to any active `receipt` series; if omitted, the existing series is preserved | | `issueDate` | string | Yes | Date of issue (YYYY-MM-DD) | | `currency` | string | Yes | Currency code | | `cashRegisterName` | string | Yes | Name or label of the cash register | | `fiscalNumber` | string | Yes | Fiscal serial number of the cash register | | `paymentMethod` | string | Yes | Payment method: `cash`, `card`, `mixed`, `other` | | `cashPayment` | number | No | Amount paid in cash | | `cardPayment` | number | No | Amount paid by card | | `otherPayment` | number | No | Amount paid by other method | | `customerName` | string | No | Customer name | | `customerCif` | string | No | Customer CIF/CUI | | `clientId` | string | No | UUID of a linked Client object | | `notes` | string | No | Public notes | | `internalNote` | string | No | Internal note | | `lines` | array | Yes | Array of line items (replaces all existing lines) | ### Line Item Object | Field | Type | Required | Description | |-------|------|----------|-------------| | `uuid` | string | No | UUID of existing line (if updating); omit to create new line | | `description` | string | Yes | Item description | | `quantity` | number | Yes | Quantity (must be > 0) | | `unitPrice` | number | Yes | Price per unit (excluding VAT) | | `vatRateId` | string | Yes | UUID of the VAT rate | | `unitOfMeasure` | string | No | Unit of measure | | `productId` | string | No | UUID of related product | ## Request ```bash {% title="cURL" %} curl -X PUT https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: company-uuid-here" \ -H "Content-Type: application/json" \ -d '{ "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "issueDate": "2026-02-18", "currency": "RON", "cashRegisterName": "Casa 1 - Front Desk", "fiscalNumber": "AAAA123456", "paymentMethod": "card", "cashPayment": 0.00, "cardPayment": 130.00, "otherPayment": 0.00, "customerName": "Acme SRL", "customerCif": "RO12345678", "notes": "Updated order", "lines": [ { "uuid": "c3d4e5f6-a7b8-9012-cdef-123456789012", "description": "Coffee - Espresso", "quantity": 2, "unitPrice": 12.61, "unitOfMeasure": "pcs", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "productId": "d4e5f6a7-b8c9-0123-defa-234567890123" }, { "description": "Pen Blue", "quantity": 5, "unitPrice": 4.20, "unitOfMeasure": "pcs", "vatRateId": "360e8400-e29b-41d4-a716-446655440000" } ] }' ``` ```javascript {% title="JavaScript" %} const response = await fetch('https://api.storno.ro/api/v1/receipts/a1b2c3d4-e5f6-7890-abcd-ef1234567890', { method: 'PUT', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': 'company-uuid-here', 'Content-Type': 'application/json' }, body: JSON.stringify({ documentSeriesId: '850e8400-e29b-41d4-a716-446655440000', issueDate: '2026-02-18', currency: 'RON', cashRegisterName: 'Casa 1 - Front Desk', fiscalNumber: 'AAAA123456', paymentMethod: 'card', cashPayment: 0.00, cardPayment: 130.00, otherPayment: 0.00, customerName: 'Acme SRL', customerCif: 'RO12345678', notes: 'Updated order', lines: [ { uuid: 'c3d4e5f6-a7b8-9012-cdef-123456789012', description: 'Coffee - Espresso', quantity: 2, unitPrice: 12.61, unitOfMeasure: 'pcs', vatRateId: '350e8400-e29b-41d4-a716-446655440000', productId: 'd4e5f6a7-b8c9-0123-defa-234567890123' }, { description: 'Pen Blue', quantity: 5, unitPrice: 4.20, unitOfMeasure: 'pcs', vatRateId: '360e8400-e29b-41d4-a716-446655440000' } ] }) }); const data = await response.json(); ``` ## Response Returns the updated receipt object with recalculated totals: ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "number": "BON-2026-042", "documentSeriesId": "850e8400-e29b-41d4-a716-446655440000", "status": "draft", "issueDate": "2026-02-18", "currency": "RON", "cashRegisterName": "Casa 1 - Front Desk", "fiscalNumber": "AAAA123456", "paymentMethod": "card", "cashPayment": "0.00", "cardPayment": "130.00", "otherPayment": "0.00", "customerName": "Acme SRL", "customerCif": "RO12345678", "notes": "Updated order", "lines": [ { "uuid": "c3d4e5f6-a7b8-9012-cdef-123456789012", "lineNumber": 1, "description": "Coffee - Espresso", "quantity": "2.00", "unitPrice": "12.61", "unitOfMeasure": "pcs", "vatRateId": "350e8400-e29b-41d4-a716-446655440000", "subtotal": "25.22", "vatAmount": "2.27", "total": "27.49" }, { "uuid": "g7h8i9j0-k1l2-3456-mnop-678901234567", "lineNumber": 2, "description": "Pen Blue", "quantity": "5.00", "unitPrice": "4.20", "unitOfMeasure": "pcs", "vatRateId": "360e8400-e29b-41d4-a716-446655440000", "subtotal": "17.65", "vatAmount": "3.35", "total": "21.00" } ], "subtotal": "42.87", "vatAmount": "5.62", "total": "48.49", "createdAt": "2026-02-18T10:14:00Z", "updatedAt": "2026-02-18T10:20:00Z" } ``` ## Line Item Behavior When updating lines: - Lines with existing `uuid` values are updated - Lines without `uuid` are created as new lines - Existing lines not included in the request are **deleted** - Line numbers are automatically reassigned sequentially ## Validation Rules ### Status Restriction - Receipt must be in `draft` status - Cannot update after it is issued, invoiced, or cancelled ### Payment Amounts - The sum of `cashPayment + cardPayment + otherPayment` must equal `total` - All payment amounts must be >= 0 ### Line Items - Minimum 1 line required - All line validation rules from the create endpoint apply ## Error Codes | Status Code | Error Code | Description | |-------------|------------|-------------| | 400 | `bad_request` | Invalid request body structure | | 401 | `unauthorized` | Missing or invalid authentication token | | 403 | `forbidden` | Invalid or missing X-Company header | | 404 | `not_found` | Receipt not found or doesn't belong to the company | | 409 | `conflict` | Receipt status prevents updates (not in draft) | | 422 | `validation_error` | Validation failed (see error details) | | 500 | `internal_error` | Server error occurred | ## Example Error Response ### Status Conflict ```json { "error": { "code": "conflict", "message": "Receipt cannot be updated", "details": { "status": "issued", "reason": "Only draft receipts can be updated", "issuedAt": "2026-02-18T10:15:00Z" } } } ``` ## Best Practices 1. **Update before issuing** - Make all corrections while in draft status; receipts become immutable after issuing 2. **Verify payment totals** - Ensure payment amounts sum correctly to the receipt total 3. **Match fiscal device** - The `fiscalNumber` and `cashRegisterName` should match the actual device that printed the receipt 4. **Correct before printing** - Update data while still in draft, before the physical receipt is printed on the fiscal device --- ## Create recurring invoice > Create a new recurring invoice template that will automatically generate invoices. URL: https://docs.storno.ro/api-reference/recurring-invoices/create # Create recurring invoice Creates a new recurring invoice template. The system will automatically generate invoices based on the specified frequency and schedule. ```http POST /api/v1/recurring-invoices ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `clientId` | string | Yes | UUID of the client | | `seriesId` | string | Yes | UUID of the document series | | `reference` | string | No | Human-readable reference | | `documentType` | string | Yes | `invoice` or `credit_note` | | `currency` | string | Yes | ISO 4217 currency code | | `invoiceTypeCode` | string | Yes | e-Factura invoice type code (e.g., "380") | | `frequency` | string | Yes | `weekly`, `biweekly`, `monthly`, `bimonthly`, `quarterly`, `semiannual`, `annual` | | `frequencyDay` | integer | Yes | Day of month for generation (1-31) | | `frequencyMonth` | integer | No | Month for annual generation (1-12, required if frequency is `annual`) | | `nextIssuanceDate` | string | Yes | ISO 8601 date for first invoice | | `stopDate` | string | No | ISO 8601 date to stop generation | | `dueDateType` | string | Yes | `fixed` or `relative` | | `dueDateDays` | integer | Conditional | Required if dueDateType is `relative` | | `dueDateFixedDay` | integer | Conditional | Required if dueDateType is `fixed` (1-31) | | `notes` | string | No | Internal notes | | `paymentTerms` | string | No | Payment terms text | | `autoEmailEnabled` | boolean | No | Auto-send email on generation (default: false) | | `autoEmailTime` | string | No | Time to send (HH:mm, default: "09:00") | | `autoEmailDayOffset` | integer | No | Days offset for email sending (default: 0) | | `penaltyEnabled` | boolean | No | Enable late payment penalties (default: false) | | `penaltyPercentPerDay` | number | No | Daily penalty percentage | | `penaltyGraceDays` | integer | No | Grace period before penalties | | `lines` | array | Yes | Array of line items (minimum 1) | ### Line Item Object | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `description` | string | Yes | Line item description | | `quantity` | number | Yes | Quantity (must be > 0) | | `unitPrice` | number | Yes | Price per unit | | `vatRateId` | string | Yes | UUID of VAT rate | | `unitOfMeasure` | string | No | Unit of measure code (default: "buc") | | `productId` | string | No | UUID of linked product | | `priceRule` | string | No | `fixed`, `exchange_rate`, or `markup` (default: "fixed") | | `referenceCurrency` | string | Conditional | Required if priceRule is `exchange_rate` | | `markupPercent` | number | Conditional | Required if priceRule is `markup` | ## Response Returns the created recurring invoice object with a `201 Created` status. ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/recurring-invoices' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "clientId": "client-uuid-1", "seriesId": "series-uuid-1", "reference": "MONTHLY-HOSTING-001", "documentType": "invoice", "currency": "RON", "invoiceTypeCode": "380", "frequency": "monthly", "frequencyDay": 1, "nextIssuanceDate": "2026-03-01", "dueDateType": "relative", "dueDateDays": 30, "notes": "Monthly hosting services", "paymentTerms": "Payment due within 30 days", "autoEmailEnabled": true, "autoEmailTime": "09:00", "penaltyEnabled": true, "penaltyPercentPerDay": 0.05, "penaltyGraceDays": 5, "lines": [ { "description": "Cloud Hosting - Business Plan", "quantity": 1, "unitPrice": 1499.00, "vatRateId": "vat-uuid-1", "unitOfMeasure": "buc", "productId": "product-uuid-1", "priceRule": "fixed" } ] }' ``` ## Example Response ```json { "uuid": "rec-inv-uuid-1", "reference": "MONTHLY-HOSTING-001", "clientId": "client-uuid-1", "seriesId": "series-uuid-1", "documentType": "invoice", "currency": "RON", "invoiceTypeCode": "380", "frequency": "monthly", "frequencyDay": 1, "frequencyMonth": null, "nextIssuanceDate": "2026-03-01", "stopDate": null, "dueDateType": "relative", "dueDateDays": 30, "dueDateFixedDay": null, "isActive": true, "notes": "Monthly hosting services", "paymentTerms": "Payment due within 30 days", "autoEmailEnabled": true, "autoEmailTime": "09:00", "autoEmailDayOffset": 0, "penaltyEnabled": true, "penaltyPercentPerDay": 0.05, "penaltyGraceDays": 5, "lines": [ { "uuid": "line-uuid-1", "description": "Cloud Hosting - Business Plan", "quantity": 1, "unitPrice": 1499.00, "vatRateId": "vat-uuid-1", "vatRate": 19, "vatAmount": 284.81, "totalAmount": 1783.81, "unitOfMeasure": "buc", "productId": "product-uuid-1", "priceRule": "fixed", "referenceCurrency": null, "markupPercent": null, "position": 1 } ], "subtotal": 1499.00, "vatTotal": 284.81, "totalAmount": 1783.81, "createdAt": "2026-02-16T10:30:00Z", "updatedAt": "2026-02-16T10:30:00Z" } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - Missing required fields (`clientId`, `seriesId`, `documentType`, `currency`, `frequency`, `nextIssuanceDate`, `dueDateType`) - Invalid frequency value - Invalid frequencyDay (must be 1-31) - Missing dueDateDays when dueDateType is `relative` - Missing dueDateFixedDay when dueDateType is `fixed` - Empty lines array - Invalid line item data (missing description, quantity ≤ 0, negative unitPrice) - Invalid currency code - Client or series not found or doesn't belong to company ## Related Endpoints - [List recurring invoices](/api-reference/recurring-invoices/list) - [Get recurring invoice](/api-reference/recurring-invoices/get) - [Update recurring invoice](/api-reference/recurring-invoices/update) --- ## Delete recurring invoice > Permanently delete a recurring invoice template. URL: https://docs.storno.ro/api-reference/recurring-invoices/delete # Delete recurring invoice Permanently deletes a recurring invoice template. This action cannot be undone. Previously generated invoices from this template are not affected. ```http DELETE /api/v1/recurring-invoices/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the recurring invoice | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns a `204 No Content` status on successful deletion with no response body. ## Example Request ```bash curl -X DELETE 'https://api.storno.ro/api/v1/recurring-invoices/rec-inv-uuid-1' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```http HTTP/1.1 204 No Content ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Recurring invoice not found or doesn't belong to company | ## Important Notes - This action is permanent and cannot be undone - Invoices previously generated from this template remain unchanged - To temporarily stop invoice generation, use the [toggle endpoint](/api-reference/recurring-invoices/toggle) instead - Deleting a recurring invoice does not delete the associated client or series ## Related Endpoints - [List recurring invoices](/api-reference/recurring-invoices/list) - [Toggle recurring invoice](/api-reference/recurring-invoices/toggle) - [Get recurring invoice](/api-reference/recurring-invoices/get) --- ## Get recurring invoice > Retrieve details of a specific recurring invoice including all line items. URL: https://docs.storno.ro/api-reference/recurring-invoices/get # Get recurring invoice Retrieves detailed information about a specific recurring invoice, including all line items that will be used as a template for generated invoices. ```http GET /api/v1/recurring-invoices/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the recurring invoice | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns a detailed recurring invoice object with embedded line items. ### Response Schema | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `reference` | string | Human-readable reference | | `clientId` | string | UUID of the client | | `client` | object | Embedded client object | | `seriesId` | string | UUID of the document series | | `series` | object | Embedded series object | | `documentType` | string | `invoice` or `credit_note` | | `currency` | string | ISO 4217 currency code | | `invoiceTypeCode` | string | e-Factura invoice type code | | `frequency` | string | Generation frequency | | `frequencyDay` | integer | Day of month for generation (1-31) | | `frequencyMonth` | integer \| null | Month for annual generation (1-12) | | `nextIssuanceDate` | string | ISO 8601 date of next generation | | `stopDate` | string \| null | ISO 8601 date to stop generation | | `dueDateType` | string | `fixed` or `relative` | | `dueDateDays` | integer \| null | Days after issue for relative due date | | `dueDateFixedDay` | integer \| null | Fixed day of month (1-31) | | `isActive` | boolean | Whether generation is enabled | | `notes` | string \| null | Internal notes | | `paymentTerms` | string \| null | Payment terms text | | `autoEmailEnabled` | boolean | Auto-send email on generation | | `autoEmailTime` | string \| null | Time to send (HH:mm) | | `autoEmailDayOffset` | integer | Days offset for email sending | | `penaltyEnabled` | boolean | Late payment penalty enabled | | `penaltyPercentPerDay` | number \| null | Daily penalty percentage | | `penaltyGraceDays` | integer \| null | Grace period before penalties | | `lines` | array | Array of recurring invoice line items | | `subtotal` | number | Sum of line items before VAT | | `vatTotal` | number | Total VAT amount | | `totalAmount` | number | Total amount including VAT | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | ### Line Item Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `description` | string | Line item description | | `quantity` | number | Quantity | | `unitPrice` | number | Price per unit | | `vatRateId` | string | UUID of VAT rate | | `vatRate` | number | VAT percentage | | `vatAmount` | number | Calculated VAT amount | | `totalAmount` | number | Line total including VAT | | `unitOfMeasure` | string | Unit of measure code | | `productId` | string \| null | UUID of linked product | | `priceRule` | string | `fixed`, `exchange_rate`, or `markup` | | `referenceCurrency` | string \| null | Currency for exchange rate pricing | | `markupPercent` | number \| null | Markup percentage | | `position` | integer | Display order | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/recurring-invoices/rec-inv-uuid-1' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json { "uuid": "rec-inv-uuid-1", "reference": "MONTHLY-001", "clientId": "client-uuid-1", "client": { "uuid": "client-uuid-1", "name": "Acme Corporation SRL", "cui": "RO12345678", "email": "contact@acme.ro" }, "seriesId": "series-uuid-1", "series": { "uuid": "series-uuid-1", "prefix": "FRE", "type": "invoice" }, "documentType": "invoice", "currency": "RON", "invoiceTypeCode": "380", "frequency": "monthly", "frequencyDay": 1, "frequencyMonth": null, "nextIssuanceDate": "2026-03-01", "stopDate": null, "dueDateType": "relative", "dueDateDays": 30, "dueDateFixedDay": null, "isActive": true, "notes": "Monthly hosting services", "paymentTerms": "Payment due within 30 days", "autoEmailEnabled": true, "autoEmailTime": "09:00", "autoEmailDayOffset": 0, "penaltyEnabled": true, "penaltyPercentPerDay": 0.05, "penaltyGraceDays": 5, "lines": [ { "uuid": "line-uuid-1", "description": "Cloud Hosting - Business Plan", "quantity": 1, "unitPrice": 1499.00, "vatRateId": "vat-uuid-1", "vatRate": 19, "vatAmount": 284.81, "totalAmount": 1783.81, "unitOfMeasure": "buc", "productId": "product-uuid-1", "priceRule": "fixed", "referenceCurrency": null, "markupPercent": null, "position": 1 } ], "subtotal": 1499.00, "vatTotal": 284.81, "totalAmount": 1783.81, "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-02-10T14:20:00Z" } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Recurring invoice not found or doesn't belong to company | ## Related Endpoints - [List recurring invoices](/api-reference/recurring-invoices/list) - [Update recurring invoice](/api-reference/recurring-invoices/update) - [Issue now](/api-reference/recurring-invoices/issue-now) --- ## Issue recurring invoice now > Manually trigger immediate invoice generation from a recurring template. URL: https://docs.storno.ro/api-reference/recurring-invoices/issue-now # Issue recurring invoice now Manually triggers immediate invoice generation from a recurring invoice template, bypassing the scheduled generation. This is useful for creating one-off invoices or testing recurring invoice configurations. ```http POST /api/v1/recurring-invoices/{uuid}/issue-now ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the recurring invoice | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns the newly created invoice object with a `201 Created` status. ### Response Schema Returns a complete invoice object with all fields from the recurring invoice template applied. | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier of the created invoice | | `number` | string | Full invoice number (e.g., "FRE00123") | | `seriesPrefix` | string | Series prefix | | `seriesNumber` | integer | Sequential number | | `clientId` | string | UUID of the client | | `documentType` | string | Document type from template | | `currency` | string | Currency from template | | `issueDate` | string | ISO 8601 date (today's date) | | `dueDate` | string | ISO 8601 date (calculated per template) | | `subtotal` | number | Sum before VAT | | `vatTotal` | number | Total VAT amount | | `totalAmount` | number | Total including VAT | | `amountPaid` | number | Amount paid (0.00 initially) | | `status` | string | Invoice status (`unpaid`) | | `recurringInvoiceId` | string | UUID of the source recurring invoice | | `lines` | array | Array of invoice line items | | `createdAt` | string | ISO 8601 creation timestamp | ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/recurring-invoices/rec-inv-uuid-1/issue-now' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json { "uuid": "invoice-uuid-1", "number": "FRE00123", "seriesPrefix": "FRE", "seriesNumber": 123, "clientId": "client-uuid-1", "client": { "uuid": "client-uuid-1", "name": "Acme Corporation SRL", "cui": "RO12345678", "email": "contact@acme.ro" }, "documentType": "invoice", "currency": "RON", "invoiceTypeCode": "380", "issueDate": "2026-02-16", "dueDate": "2026-03-18", "subtotal": 1499.00, "vatTotal": 284.81, "totalAmount": 1783.81, "amountPaid": 0.00, "status": "unpaid", "notes": "Monthly hosting services", "paymentTerms": "Payment due within 30 days", "recurringInvoiceId": "rec-inv-uuid-1", "lines": [ { "uuid": "inv-line-uuid-1", "description": "Cloud Hosting - Business Plan", "quantity": 1, "unitPrice": 1499.00, "vatRate": 19, "vatAmount": 284.81, "totalAmount": 1783.81, "unitOfMeasure": "buc", "productId": "product-uuid-1", "position": 1 } ], "createdAt": "2026-02-16T12:30:00Z", "updatedAt": "2026-02-16T12:30:00Z" } ``` ## Behavior ### Invoice Creation - Creates a new invoice using the current date as `issueDate` - Calculates `dueDate` based on the recurring invoice's `dueDateType` and related fields - Uses the next available number from the specified series - Copies all line items from the recurring invoice template - Sets `status` to `unpaid` and `amountPaid` to 0.00 - Links the invoice to the recurring invoice via `recurringInvoiceId` ### Next Issuance Date The recurring invoice's `nextIssuanceDate` is **not automatically updated**. Manual issuance does not affect the scheduled generation cycle. ### Auto-Email If `autoEmailEnabled` is `true` on the recurring invoice, the system will send the invoice email according to the configured `autoEmailTime` and `autoEmailDayOffset`. ### Dynamic Pricing If line items use `priceRule` of `exchange_rate` or `markup`, prices are calculated at the time of invoice generation using current exchange rates or markup rules. ## Use Cases ### Testing Test a recurring invoice configuration before activating automatic generation. ### One-Off Generation Generate an invoice outside the normal schedule, such as for mid-month billing adjustments. ### Early Billing Issue the next invoice early if a client requests it or payment is needed sooner than scheduled. ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Recurring invoice not found or doesn't belong to company | | 422 | validation_error | Cannot generate invoice (e.g., inactive series, invalid client) | ## Related Endpoints - [Get recurring invoice](/api-reference/recurring-invoices/get) - [List invoices](/api-reference/invoices/list) - [Get invoice](/api-reference/invoices/get) --- ## List recurring invoices > Retrieve a paginated list of recurring invoices with optional filtering. URL: https://docs.storno.ro/api-reference/recurring-invoices/list # List recurring invoices Retrieves a paginated list of recurring invoices for the authenticated company. Results can be filtered by status, frequency, and search terms. ```http GET /api/v1/recurring-invoices ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `page` | integer | No | Page number (default: 1) | | `limit` | integer | No | Items per page (default: 20, max: 100) | | `search` | string | No | Search term to filter by reference or notes | | `isActive` | boolean | No | Filter by active status (true/false) | | `frequency` | string | No | Filter by frequency: `weekly`, `biweekly`, `monthly`, `bimonthly`, `quarterly`, `semiannual`, `annual` | ## Response Returns a paginated list of recurring invoice objects. ### Response Schema | Field | Type | Description | |-------|------|-------------| | `data` | array | Array of recurring invoice objects | | `total` | integer | Total number of matching recurring invoices | | `page` | integer | Current page number | | `limit` | integer | Items per page | | `pages` | integer | Total number of pages | ### Recurring Invoice Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `reference` | string | Human-readable reference | | `clientId` | string | UUID of the client | | `clientName` | string | Client display name | | `seriesId` | string | UUID of the document series | | `seriesPrefix` | string | Series prefix for generated invoices | | `documentType` | string | `invoice` or `credit_note` | | `currency` | string | ISO 4217 currency code | | `invoiceTypeCode` | string | e-Factura invoice type code | | `frequency` | string | Generation frequency | | `frequencyDay` | integer | Day of month for generation (1-31) | | `frequencyMonth` | integer | Month for annual generation (1-12) | | `nextIssuanceDate` | string | ISO 8601 date of next generation | | `stopDate` | string \| null | ISO 8601 date to stop generation | | `dueDateType` | string | `fixed` or `relative` | | `dueDateDays` | integer \| null | Days after issue for relative due date | | `dueDateFixedDay` | integer \| null | Fixed day of month (1-31) | | `isActive` | boolean | Whether generation is enabled | | `notes` | string \| null | Internal notes | | `paymentTerms` | string \| null | Payment terms text | | `autoEmailEnabled` | boolean | Auto-send email on generation | | `autoEmailTime` | string \| null | Time to send (HH:mm) | | `autoEmailDayOffset` | integer | Days offset for email sending | | `penaltyEnabled` | boolean | Late payment penalty enabled | | `penaltyPercentPerDay` | number \| null | Daily penalty percentage | | `penaltyGraceDays` | integer \| null | Grace period before penalties | | `totalAmount` | number | Template total amount | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/recurring-invoices?page=1&limit=20&isActive=true' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json { "data": [ { "uuid": "rec-inv-uuid-1", "reference": "MONTHLY-001", "clientId": "client-uuid-1", "clientName": "Acme Corporation SRL", "seriesId": "series-uuid-1", "seriesPrefix": "FRE", "documentType": "invoice", "currency": "RON", "invoiceTypeCode": "380", "frequency": "monthly", "frequencyDay": 1, "frequencyMonth": null, "nextIssuanceDate": "2026-03-01", "stopDate": null, "dueDateType": "relative", "dueDateDays": 30, "dueDateFixedDay": null, "isActive": true, "notes": "Monthly hosting services", "paymentTerms": "Payment due within 30 days", "autoEmailEnabled": true, "autoEmailTime": "09:00", "autoEmailDayOffset": 0, "penaltyEnabled": true, "penaltyPercentPerDay": 0.05, "penaltyGraceDays": 5, "totalAmount": 1499.00, "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-02-10T14:20:00Z" } ], "total": 15, "page": 1, "limit": 20, "pages": 1 } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 422 | validation_error | Invalid query parameters | ## Related Endpoints - [Get recurring invoice](/api-reference/recurring-invoices/get) - [Create recurring invoice](/api-reference/recurring-invoices/create) - [Update recurring invoice](/api-reference/recurring-invoices/update) --- ## Toggle recurring invoice > Enable or disable automatic invoice generation for a recurring invoice. URL: https://docs.storno.ro/api-reference/recurring-invoices/toggle # Toggle recurring invoice Toggles the `isActive` flag of a recurring invoice, enabling or disabling automatic invoice generation. This provides a way to pause and resume recurring invoices without deleting them. ```http POST /api/v1/recurring-invoices/{uuid}/toggle ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the recurring invoice | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns the updated recurring invoice object with the toggled `isActive` status. ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/recurring-invoices/rec-inv-uuid-1/toggle' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json { "uuid": "rec-inv-uuid-1", "reference": "MONTHLY-HOSTING-001", "clientId": "client-uuid-1", "seriesId": "series-uuid-1", "documentType": "invoice", "currency": "RON", "invoiceTypeCode": "380", "frequency": "monthly", "frequencyDay": 1, "frequencyMonth": null, "nextIssuanceDate": "2026-03-01", "stopDate": null, "dueDateType": "relative", "dueDateDays": 30, "dueDateFixedDay": null, "isActive": false, "notes": "Monthly hosting services", "paymentTerms": "Payment due within 30 days", "autoEmailEnabled": true, "autoEmailTime": "09:00", "autoEmailDayOffset": 0, "penaltyEnabled": true, "penaltyPercentPerDay": 0.05, "penaltyGraceDays": 5, "lines": [ { "uuid": "line-uuid-1", "description": "Cloud Hosting - Business Plan", "quantity": 1, "unitPrice": 1499.00, "vatRateId": "vat-uuid-1", "vatRate": 19, "vatAmount": 284.81, "totalAmount": 1783.81, "unitOfMeasure": "buc", "productId": "product-uuid-1", "priceRule": "fixed", "referenceCurrency": null, "markupPercent": null, "position": 1 } ], "subtotal": 1499.00, "vatTotal": 284.81, "totalAmount": 1783.81, "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-02-16T12:15:00Z" } ``` ## Behavior - If `isActive` is `true`, it will be set to `false` (pausing generation) - If `isActive` is `false`, it will be set to `true` (resuming generation) - The `updatedAt` timestamp is updated - The `nextIssuanceDate` is not modified - When re-enabled, if `nextIssuanceDate` is in the past, the next scheduled job will generate the invoice immediately ## Use Cases ### Temporary Pause When a client requests a temporary suspension of service, toggle the recurring invoice off instead of deleting it. Toggle it back on when service resumes. ### Seasonal Services For services that are seasonal or have scheduled breaks, toggle recurring invoices instead of creating and deleting them repeatedly. ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Recurring invoice not found or doesn't belong to company | ## Related Endpoints - [Get recurring invoice](/api-reference/recurring-invoices/get) - [Update recurring invoice](/api-reference/recurring-invoices/update) - [Delete recurring invoice](/api-reference/recurring-invoices/delete) --- ## Update recurring invoice > Update an existing recurring invoice template. URL: https://docs.storno.ro/api-reference/recurring-invoices/update # Update recurring invoice Updates an existing recurring invoice template. All fields are optional, but at least one must be provided. ```http PUT /api/v1/recurring-invoices/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the recurring invoice | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters All parameters are optional, but at least one must be provided. | Parameter | Type | Description | |-----------|------|-------------| | `clientId` | string | UUID of the client | | `seriesId` | string | UUID of the document series | | `reference` | string | Human-readable reference | | `documentType` | string | `invoice` or `credit_note` | | `currency` | string | ISO 4217 currency code | | `invoiceTypeCode` | string | e-Factura invoice type code | | `frequency` | string | `weekly`, `biweekly`, `monthly`, `bimonthly`, `quarterly`, `semiannual`, `annual` | | `frequencyDay` | integer | Day of month for generation (1-31) | | `frequencyMonth` | integer | Month for annual generation (1-12) | | `nextIssuanceDate` | string | ISO 8601 date for next invoice | | `stopDate` | string \| null | ISO 8601 date to stop generation (null to remove) | | `dueDateType` | string | `fixed` or `relative` | | `dueDateDays` | integer | Days after issue for relative due date | | `dueDateFixedDay` | integer | Fixed day of month (1-31) | | `notes` | string \| null | Internal notes | | `paymentTerms` | string \| null | Payment terms text | | `autoEmailEnabled` | boolean | Auto-send email on generation | | `autoEmailTime` | string | Time to send (HH:mm) | | `autoEmailDayOffset` | integer | Days offset for email sending | | `penaltyEnabled` | boolean | Enable late payment penalties | | `penaltyPercentPerDay` | number | Daily penalty percentage | | `penaltyGraceDays` | integer | Grace period before penalties | | `lines` | array | Array of line items | ### Line Item Object When updating lines, the entire lines array must be provided (it replaces existing lines). | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `description` | string | Yes | Line item description | | `quantity` | number | Yes | Quantity (must be > 0) | | `unitPrice` | number | Yes | Price per unit | | `vatRateId` | string | Yes | UUID of VAT rate | | `unitOfMeasure` | string | No | Unit of measure code (default: "buc") | | `productId` | string | No | UUID of linked product | | `priceRule` | string | No | `fixed`, `exchange_rate`, or `markup` (default: "fixed") | | `referenceCurrency` | string | Conditional | Required if priceRule is `exchange_rate` | | `markupPercent` | number | Conditional | Required if priceRule is `markup` | ## Response Returns the updated recurring invoice object. ## Example Request ```bash curl -X PUT 'https://api.storno.ro/api/v1/recurring-invoices/rec-inv-uuid-1' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "reference": "MONTHLY-HOSTING-002", "nextIssuanceDate": "2026-04-01", "penaltyPercentPerDay": 0.10, "lines": [ { "description": "Cloud Hosting - Premium Plan", "quantity": 1, "unitPrice": 2499.00, "vatRateId": "vat-uuid-1", "unitOfMeasure": "buc", "productId": "product-uuid-2", "priceRule": "fixed" } ] }' ``` ## Example Response ```json { "uuid": "rec-inv-uuid-1", "reference": "MONTHLY-HOSTING-002", "clientId": "client-uuid-1", "seriesId": "series-uuid-1", "documentType": "invoice", "currency": "RON", "invoiceTypeCode": "380", "frequency": "monthly", "frequencyDay": 1, "frequencyMonth": null, "nextIssuanceDate": "2026-04-01", "stopDate": null, "dueDateType": "relative", "dueDateDays": 30, "dueDateFixedDay": null, "isActive": true, "notes": "Monthly hosting services", "paymentTerms": "Payment due within 30 days", "autoEmailEnabled": true, "autoEmailTime": "09:00", "autoEmailDayOffset": 0, "penaltyEnabled": true, "penaltyPercentPerDay": 0.10, "penaltyGraceDays": 5, "lines": [ { "uuid": "line-uuid-2", "description": "Cloud Hosting - Premium Plan", "quantity": 1, "unitPrice": 2499.00, "vatRateId": "vat-uuid-1", "vatRate": 19, "vatAmount": 474.81, "totalAmount": 2973.81, "unitOfMeasure": "buc", "productId": "product-uuid-2", "priceRule": "fixed", "referenceCurrency": null, "markupPercent": null, "position": 1 } ], "subtotal": 2499.00, "vatTotal": 474.81, "totalAmount": 2973.81, "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-02-16T11:45:00Z" } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Recurring invoice not found or doesn't belong to company | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - No fields provided for update - Invalid frequency value - Invalid frequencyDay (must be 1-31) - Invalid date format - Empty lines array - Invalid line item data - Client or series not found ## Related Endpoints - [Get recurring invoice](/api-reference/recurring-invoices/get) - [Delete recurring invoice](/api-reference/recurring-invoices/delete) - [Toggle recurring invoice](/api-reference/recurring-invoices/toggle) --- ## Balance Analysis URL: https://docs.storno.ro/api-reference/reports/balance-analysis # Balance Analysis API Upload monthly trial balance PDFs (Balanta de verificare) and analyze financial indicators. ## Endpoints ### Upload Balance ``` POST /api/v1/balances/upload ``` Upload one or more trial balance PDFs. Supports **multi-file upload**. Year, month, and company CUI are **auto-detected** from each PDF. If a balance already exists for the same company/year/month, it is replaced. Duplicate files (same content hash) are rejected. The CUI detected from each PDF is validated against the selected company. **Request:** `multipart/form-data` | Field | Type | Required | Description | |-------|------|----------|-------------| | `files[]` | file[] | Yes* | One or more PDF files (max 10MB each) | | `file` | file | Yes* | Single PDF file (backwards compatible) | \* Provide either `files[]` or `file`. **Response:** `201 Created` ```json { "results": [ { "filename": "balanta-iunie-2025.pdf", "success": true, "id": "0192d3e4-...", "year": 2025, "month": 6, "status": "pending" }, { "filename": "balanta-iulie-2025.pdf", "success": false, "error": "This file has already been uploaded.", "code": "DUPLICATE_FILE" } ] } ``` **Per-file error codes:** | Code | Description | |------|-------------| | `DUPLICATE_FILE` | File with same content hash already uploaded | | `CUI_MISMATCH` | PDF CUI doesn't match selected company | | `INVALID_TYPE` | File is not a PDF | | `FILE_TOO_LARGE` | File exceeds 10MB | | `PARSE_ERROR` | Failed to parse PDF content | | `NO_YEAR` | Could not detect year from PDF | | `NO_MONTH` | Could not detect month from PDF | Row parsing is processed asynchronously in the background. Status transitions: `pending` -> `processing` -> `completed` | `failed`. --- ### List Balances ``` GET /api/v1/balances?year=2025 ``` List all uploaded balances for a company and year. **Query Parameters:** | Param | Type | Default | Description | |-------|------|---------|-------------| | `year` | integer | current year | Filter by year | **Response:** `200 OK` ```json [ { "id": "0192d3e4-...", "year": 2025, "month": 1, "status": "completed", "totalAccounts": 85, "originalFilename": "balanta-ian-2025.pdf", "sourceSoftware": "SAGA", "error": null, "processedAt": "2025-02-01T10:05:00+00:00", "createdAt": "2025-02-01T10:00:00+00:00" } ] ``` --- ### Get Balance ``` GET /api/v1/balances/{id} ``` Get details of a single balance. **Response:** `200 OK` (same structure as list item) --- ### Get Balance Rows ``` GET /api/v1/balances/{id}/rows ``` Get the parsed account rows for a balance. Useful for debugging and verifying PDF parsing results. **Response:** `200 OK` ```json [ { "accountCode": "411", "accountName": "Clienti", "initialDebit": "5000.00", "initialCredit": "0.00", "previousDebit": "12000.00", "previousCredit": "8000.00", "currentDebit": "3000.00", "currentCredit": "2000.00", "totalDebit": "15000.00", "totalCredit": "10000.00", "finalDebit": "10000.00", "finalCredit": "5000.00" } ] ``` --- ### Delete Balance ``` DELETE /api/v1/balances/{id} ``` Soft-delete a balance and its parsed rows. **Response:** `204 No Content` --- ### Reprocess Balance ``` POST /api/v1/balances/{id}/reprocess ``` Re-parse a balance PDF. Useful after parser improvements to re-extract data from previously uploaded files. **Response:** `200 OK` ```json { "message": "Reprocessing started", "id": "0192d3e4-...", "status": "pending" } ``` --- ### Balance Analysis ``` GET /api/v1/balances/analysis?year=2025 ``` Compute financial analysis from all completed balances for a year. **Query Parameters:** | Param | Type | Default | Description | |-------|------|---------|-------------| | `year` | integer | current year | Analysis year | **Response:** `200 OK` ```json { "year": 2025, "balances": [ { "id": "...", "month": 1, "status": "completed", "totalAccounts": 85, "originalFilename": "balanta-ian.pdf", "processedAt": "...", "uploadedAt": "..." } ], "indicators": { "revenue": "850000.00", "expenses": "720000.00", "netProfit": "130000.00", "turnover": "800000.00", "salaries": "280000.00", "profitTax": "20800.00", "supplierDebts": "45000.00", "clientReceivables": "120000.00", "bankBalance": "95000.00", "cashBalance": "5200.00" }, "monthlyEvolution": [ { "month": 1, "revenue": "70000.00", "expenses": "60000.00", "profit": "10000.00" }, { "month": 2, "revenue": "75000.00", "expenses": "62000.00", "profit": "13000.00" } ], "profitability": { "profitMargin": 15.3, "expenseRatio": 84.7, "salaryRatio": 32.9 }, "topExpenses": [ { "accountCode": "641", "accountName": "Cheltuieli cu salariile", "amount": "280000.00", "percentage": 38.9 }, { "accountCode": "612", "accountName": "Cheltuieli cu chiriile", "amount": "96000.00", "percentage": 13.3 } ], "yoyComparison": { "currentYear": 2025, "previousYear": 2024, "current": { "revenue": "850000.00", "expenses": "720000.00", "profit": "130000.00" }, "previous": { "revenue": "780000.00", "expenses": "680000.00", "profit": "100000.00" }, "changes": { "revenue": 8.97, "expenses": 5.88, "profit": 30.0 } }, "balanceSheet": { "hasData": true, "currentAssets": "220200.00", "fixedAssets": "180000.00", "totalAssets": "400200.00", "inventory": "32000.00", "receivables": "120000.00", "cash": "5200.00", "bank": "95000.00", "securities": "0.00", "currentLiabilities": "98500.00", "longTermDebt": "60000.00", "totalLiabilities": "158500.00", "supplierDebts": "45000.00", "salaryDebts": "18000.00", "taxDebts": "22000.00", "vatPayable": "8500.00", "shortTermDebt": "13500.00", "equity": "241700.00", "depreciation": "12000.00", "interestExpense": "3400.00", "cogs": "320000.00", "servicesExpense": "85000.00" }, "liquidity": { "hasData": true, "currentRatio": { "value": 2.24, "status": "normal" }, "quickRatio": { "value": 1.91, "status": "normal" }, "cashRatio": { "value": 1.02, "status": "normal" }, "workingCapital": { "value": "121700.00", "status": "normal" }, "workingCapitalLongTerm": { "value": "121700.00", "status": "normal" }, "workingCapitalRequirement": { "value": "67000.00", "status": "normal" }, "netCash": { "value": "54700.00", "status": "normal" } }, "solvency": { "hasData": true, "debtToEquity": { "value": 0.66, "status": "normal" }, "financialAutonomy": { "value": 60.4, "status": "normal" }, "debtRatio": { "value": 39.6, "status": "normal" }, "generalSolvency": { "value": 2.52, "status": "normal" }, "interestCoverage": { "value": 45.6, "status": "normal" } }, "profitabilityRatios": { "hasData": true, "grossMargin": { "value": 49.4, "status": "normal" }, "operatingMargin": { "value": 18.7, "status": "normal" }, "ebitdaMargin": { "value": 20.2, "status": "normal" }, "netMargin": { "value": 15.3, "status": "normal" }, "returnOnAssets": { "value": 32.5, "status": "normal" }, "returnOnEquity": { "value": 53.8, "status": "normal" }, "returnOnCapitalEmployed": { "value": 52.9, "status": "normal" }, "ebit": "154200.00", "ebitda": "166200.00" }, "efficiency": { "hasData": true, "assetTurnover": { "value": 2.12, "status": "normal" }, "fixedAssetTurnover": { "value": 4.72, "status": "normal" }, "inventoryTurnover": { "value": 10.0, "status": "normal" }, "inventoryDays": { "value": 37, "status": "normal" }, "dso": { "value": 51, "status": "normal" }, "dpo": { "value": 41, "status": "warning" }, "cashConversionCycle": { "value": 47, "status": "normal" } }, "fiscal": { "hasData": true, "vatPayable": { "value": "8500.00", "status": "warning" }, "salaryDebts": { "value": "18000.00", "status": "warning" }, "stateTaxDebts": { "value": "22000.00", "status": "warning" }, "microThreshold": { "isMicro": false, "plafonEur": 250000, "plafonRon": "1250000.00", "revenueEur": "170000.00", "usagePercent": 68.0, "status": "normal" }, "vatThreshold": { "isVatPayer": true, "plafonRon": "300000.00", "usagePercent": 283.3, "status": "na" } }, "cashflow": { "hasData": true, "cashRunwayMonths": { "value": 1.39, "status": "critical" }, "monthlyBurnRate": { "value": "10833.33", "status": "normal" }, "breakEvenRevenue": { "value": "204000.00", "status": "normal" }, "breakEvenMonths": { "value": 2.9, "status": "normal" }, "contributionRatePercent": 52.4, "operatingLeverage": { "value": 2.43, "status": "normal" } }, "aging": { "buckets": [ { "range": "0-30", "amount": "82000.00" }, { "range": "31-60", "amount": "25000.00" }, { "range": "61-90", "amount": "8000.00" }, { "range": "90+", "amount": "5000.00" } ], "totalUnpaid": "120000.00", "totalCount": 18, "countOver90": 2, "percentOver90": 4.2, "overdueStatus": "normal", "estimatedProvision": "1900.00" }, "concentration": { "top5SharePercent": 62.4, "top10SharePercent": 81.7, "top5Status": "warning", "topClients": [ { "name": "SC EXEMPLE SRL", "revenue": "180000.00", "percent": 21.2 }, { "name": "ALT CLIENT SRL", "revenue": "120000.00", "percent": 14.1 } ], "totalRevenue": "850000.00" } } ``` ## Grouped Ratios The response also includes 8 grouped indicator sections. Each ratio is `{ value: number | null, status: 'normal' | 'warning' | 'critical' | 'na' }`. Each amount is `{ value: string | null, status }`. Sub-objects backed by the trial balance carry `hasData: false` until at least one balance is uploaded; aging/concentration are sourced from invoice data and therefore always populated. | Section | Fields | Computed from | |---------|--------|---------------| | `liquidity` | `currentRatio`, `quickRatio`, `cashRatio`, `workingCapital`, `workingCapitalLongTerm`, `workingCapitalRequirement`, `netCash` | Trial balance — active circulante / datorii curente | | `solvency` | `debtToEquity`, `financialAutonomy`, `debtRatio`, `generalSolvency`, `interestCoverage` | Trial balance — capitaluri proprii, datorii, EBIT | | `profitabilityRatios` | `grossMargin`, `operatingMargin`, `ebitdaMargin`, `netMargin`, `returnOnAssets`, `returnOnEquity`, `returnOnCapitalEmployed`, plus `ebit` / `ebitda` raw amounts | Trial balance — CA, costuri, profit | | `efficiency` | `assetTurnover`, `fixedAssetTurnover`, `inventoryTurnover`, `inventoryDays`, `dso`, `dpo`, `cashConversionCycle` | Trial balance — annualised | | `fiscal` | `vatPayable`, `salaryDebts`, `stateTaxDebts`, `microThreshold` (`isMicro`, `plafonEur`, `plafonRon`, `revenueEur`, `usagePercent`, `status`), `vatThreshold` (`isVatPayer`, `plafonRon`, `usagePercent`, `status`) | Trial balance + Company.vatPayer + EUR/RON rate from `ExchangeRateService` | | `cashflow` | `cashRunwayMonths`, `monthlyBurnRate`, `breakEvenRevenue`, `breakEvenMonths`, `contributionRatePercent`, `operatingLeverage` | Trial balance + monthly evolution | | `aging` | `buckets[]` (`0-30`, `31-60`, `61-90`, `90+`), `totalUnpaid`, `totalCount`, `countOver90`, `percentOver90`, `overdueStatus`, `estimatedProvision` | `invoice` table — outgoing, unpaid, non-cancelled | | `concentration` | `top5SharePercent`, `top10SharePercent`, `top5Status`, `topClients[]`, `totalRevenue` | `invoice` table — outgoing, current year | Romanian thresholds are configured at the top of `BalanceAnalysisService` (`MICRO_THRESHOLD_EUR = 250000`, `VAT_THRESHOLD_RON = 300000`, `SAFE_RUNWAY_MONTHS = 6`). ## Indicator Computation Indicators are computed from the **latest month's** trial balance rows using the Romanian chart of accounts. **P&L accounts (classes 6-7)** use **cumulative turnover columns** (`currentDebit`/`currentCredit`) instead of final balance columns. This handles Romanian trial balances where monthly closing entries make `finalDebit == finalCredit` for P&L accounts, and year-end closings zero out final balances entirely. **Balance sheet accounts (classes 1-5)** use `GREATEST(finalDebit, finalCredit)` to handle potential column order inconsistencies from PDF text extraction. | Indicator | Account Code Pattern | Column Used | |-----------|---------------------|-------------| | Revenue | `7%` | SUM(currentCredit) | | Expenses | `6%` | SUM(currentDebit) | | Net Profit | - | Revenue - Expenses | | Turnover | `70%`, `71%` | SUM(currentCredit) | | Salaries | `641%`, `642%` | SUM(currentDebit) | | Profit Tax | `691%` | SUM(currentDebit) | | Supplier Debts | `401%` | SUM(GREATEST(finalCredit, finalDebit)) | | Client Receivables | `411%` | SUM(GREATEST(finalDebit, finalCredit)) | | Bank Balance | `5121%` | SUM(GREATEST(finalDebit, finalCredit)) | | Cash Balance | `5311%` | SUM(GREATEST(finalDebit, finalCredit)) | ## Permissions All endpoints require `REPORT_VIEW` permission and the `canViewReports()` license check. ## PDF Parsing Supported accounting software exports: SAGA C, Ciel, FGO, WinMentor, Nexus, Charme, ASiS. The parser extracts: - Period (year/month) from header - Company CUI (validated against selected company) - Source software detection - Account rows with 10 numeric columns (initial D/C, previous D/C, current D/C, total D/C, final D/C) Number format support: - Romanian: `1.234.567,89` (dots as thousands, comma as decimal) - Space-separated: `1 101 657.93` (spaces as thousands, dot as decimal) - Standard: `1234.56` - Standalone dashes (`-`, `–`, `—`) are treated as zero values --- ## Sales Analysis Report > Generate sales analysis reports for a date range URL: https://docs.storno.ro/api-reference/reports/sales-analysis # Sales Analysis Report Generate a comprehensive sales analysis report for a specified date range, including KPI summaries, monthly revenue trends, recent invoices, top clients, and top products. --- ## Generate Sales Analysis Report ```http GET /api/v1/reports/sales ``` Returns a full sales analysis covering the requested period, including year-over-year KPI comparisons, monthly revenue breakdowns, and rankings for clients and products. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | | X-Company | string | Yes | Company UUID | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | dateFrom | string | Yes | Start date in YYYY-MM-DD format (e.g., 2026-01-01) | | dateTo | string | Yes | End date in YYYY-MM-DD format (e.g., 2026-02-26) | ### Response ```json { "period": { "dateFrom": "2026-01-01", "dateTo": "2026-02-26" }, "kpiSummary": { "annualTotal": { "amount": 1250000.00, "year": 2026, "prevAmount": 980000.00, "prevYear": 2025 }, "periodInvoiced": { "subtotal": 210084.03, "vatTotal": 39915.97, "total": 250000.00, "count": 42 }, "periodCollected": { "subtotal": 168067.23, "vatTotal": 31932.77, "total": 200000.00, "count": 35 }, "periodOutstanding": { "subtotal": 42016.81, "vatTotal": 7983.19, "total": 50000.00, "count": 7 } }, "monthlyRevenue": [ { "month": "2026-01", "invoiced": 130000.00, "collected": 110000.00 }, { "month": "2026-02", "invoiced": 120000.00, "collected": 90000.00 } ], "recentInvoices": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "number": "FINV2026042", "issueDate": "2026-02-24", "clientName": "SC Example SRL", "total": 11900.00, "currency": "RON", "status": "paid", "paidAt": "2026-02-25" }, { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "number": "FINV2026041", "issueDate": "2026-02-20", "clientName": "SC Another SRL", "total": 5950.00, "currency": "RON", "status": "unpaid", "paidAt": null } ], "topClients": [ { "clientId": "c3d4e5f6-a7b8-9012-cdef-123456789012", "clientName": "SC Top Client SRL", "total": 95000.00, "count": 12 }, { "clientId": "d4e5f6a7-b8c9-0123-defa-234567890123", "clientName": "SC Second Client SRL", "total": 72000.00, "count": 8 } ], "topProducts": [ { "description": "Software Development Services", "productCode": "SVC-001", "total": 80000.00, "quantity": 400 }, { "description": "Consulting Hours", "productCode": "SVC-002", "total": 45000.00, "quantity": 150 } ] } ``` ### Response Fields #### `period` | Field | Type | Description | |-------|------|-------------| | dateFrom | string | Start of the report period (YYYY-MM-DD) | | dateTo | string | End of the report period (YYYY-MM-DD) | #### `kpiSummary` | Field | Type | Description | |-------|------|-------------| | annualTotal | object | Year-to-date totals for the current and previous year | | periodInvoiced | object | Invoiced amounts within the requested date range | | periodCollected | object | Collected (paid) amounts within the requested date range | | periodOutstanding | object | Unpaid amounts within the requested date range | ##### `annualTotal` | Field | Type | Description | |-------|------|-------------| | amount | number | Total invoiced amount for the current year | | year | integer | Current year | | prevAmount | number | Total invoiced amount for the previous year | | prevYear | integer | Previous year | ##### `periodInvoiced` / `periodCollected` / `periodOutstanding` | Field | Type | Description | |-------|------|-------------| | subtotal | number | Amount excluding VAT | | vatTotal | number | VAT amount | | total | number | Total amount including VAT | | count | integer | Number of invoices in this bucket | #### `monthlyRevenue` Array of monthly revenue breakdown objects. | Field | Type | Description | |-------|------|-------------| | month | string | Month in YYYY-MM format | | invoiced | number | Total invoiced amount (including VAT) for the month | | collected | number | Total collected amount (including VAT) for the month | #### `recentInvoices` Array of the most recent invoices in the period, ordered by issue date descending. | Field | Type | Description | |-------|------|-------------| | id | string | Invoice UUID | | number | string | Invoice number | | issueDate | string | Issue date (YYYY-MM-DD) | | clientName | string | Client display name | | total | number | Total invoice amount (including VAT) | | currency | string | Currency code (e.g., RON, EUR) | | status | string | Invoice status: `paid`, `unpaid`, `overdue`, `draft` | | paidAt | string \| null | Payment date (YYYY-MM-DD), or null if unpaid | #### `topClients` Array of top clients ranked by total invoiced amount in the period. | Field | Type | Description | |-------|------|-------------| | clientId | string | Client UUID | | clientName | string | Client display name | | total | number | Total invoiced amount (including VAT) | | count | integer | Number of invoices issued to this client | #### `topProducts` Array of top products/services ranked by total invoiced amount in the period. | Field | Type | Description | |-------|------|-------------| | description | string | Product or service description | | productCode | string | Product code or SKU | | total | number | Total invoiced amount for this product | | quantity | number | Total quantity invoiced | ### Error Responses | Status | Description | |--------|-------------| | 400 | Bad request - missing or invalid `dateFrom` / `dateTo` parameters | | 402 | Plan limit exceeded - feature not available on current subscription plan | | 403 | Forbidden - authenticated user has no access to the specified company | | 404 | Company not found - the specified company UUID does not exist | --- ## VAT Report > Generate monthly VAT reports for tax filing URL: https://docs.storno.ro/api-reference/reports/vat-report # VAT Report Generate detailed VAT (TVA) reports for a specific month to support tax filing requirements. --- ## Generate VAT Report ```http GET /api/v1/reports/vat ``` Generate a comprehensive VAT report including sales, purchases, VAT collected, and VAT deductible. ### Headers | Name | Type | Required | Description | |------|------|----------|-------------| | Authorization | string | Yes | Bearer token for authentication | | X-Company | string | Yes | Company UUID | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | year | integer | Yes | Report year (e.g., 2026) | | month | integer | Yes | Report month (1-12) | ### Response ```json { "period": "2026-02", "totalSales": 450000.00, "totalVatCollected": 85500.00, "totalPurchases": 180000.00, "totalVatDeductible": 34200.00, "vatDue": 51300.00, "invoices": [ { "number": "FINV2026001", "type": "sale", "date": "2026-02-03", "client": "SC Example SRL", "cif": "12345678", "baseAmount": 10000.00, "vatRate": 19, "vatAmount": 1900.00, "totalAmount": 11900.00 }, { "number": "FINV2026002", "type": "purchase", "date": "2026-02-05", "supplier": "SC Supplier SRL", "cif": "87654321", "baseAmount": 5000.00, "vatRate": 19, "vatAmount": 950.00, "totalAmount": 5950.00 } ] } ``` ### Response Fields #### Summary | Field | Type | Description | |-------|------|-------------| | period | string | Report period in YYYY-MM format | | totalSales | number | Total sales revenue (excluding VAT) | | totalVatCollected | number | Total VAT collected on sales | | totalPurchases | number | Total purchases (excluding VAT) | | totalVatDeductible | number | Total VAT paid on purchases | | vatDue | number | Net VAT due (collected - deductible) | #### Invoice Details | Field | Type | Description | |-------|------|-------------| | number | string | Invoice number | | type | string | Transaction type: `sale` or `purchase` | | date | string | Invoice date (YYYY-MM-DD) | | client | string | Client name (for sales) | | supplier | string | Supplier name (for purchases) | | cif | string | Company fiscal code | | baseAmount | number | Base amount (excluding VAT) | | vatRate | integer | VAT rate percentage | | vatAmount | number | VAT amount | | totalAmount | number | Total amount (including VAT) | ### Error Responses | Status | Description | |--------|-------------| | 401 | Unauthorized - invalid authentication | | 403 | Forbidden - no access to company | | 422 | Invalid parameters - year and month required | --- ## Delete supplier > Soft-delete a supplier record. URL: https://docs.storno.ro/api-reference/suppliers/delete # Delete supplier Soft-deletes a supplier record. The supplier is marked as deleted but not permanently removed from the database. Incoming invoices associated with this supplier remain unchanged. ```http DELETE /api/v1/suppliers/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the supplier | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns a `204 No Content` status on successful deletion with no response body. ## Example Request ```bash curl -X DELETE 'https://api.storno.ro/api/v1/suppliers/supplier-uuid-1' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```http HTTP/1.1 204 No Content ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Supplier not found or doesn't belong to company | | 409 | conflict | Cannot delete supplier with active references | ## Soft Delete Behavior This endpoint performs a soft delete: - The supplier record is marked as deleted (`deletedAt` timestamp is set) - The supplier no longer appears in list endpoints - Incoming invoices associated with this supplier are not affected - The supplier can potentially be restored by support if needed - If the supplier sends new invoices via ANAF, they will be recreated ## Important Notes - This is a soft delete operation - data is not permanently removed - Existing incoming invoices from this supplier remain intact - Invoice history and statistics are preserved - If new invoices arrive from this supplier via ANAF sync, the supplier record will be restored automatically ## Related Endpoints - [Get supplier](/api-reference/suppliers/get) - [Update supplier](/api-reference/suppliers/update) - [List suppliers](/api-reference/suppliers/list) --- ## Get supplier > Retrieve detailed information about a specific supplier including invoice history. URL: https://docs.storno.ro/api-reference/suppliers/get # Get supplier Retrieves detailed information about a specific supplier, including summary statistics of incoming invoices. ```http GET /api/v1/suppliers/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the supplier | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns a detailed supplier object with incoming invoice summary statistics. ### Response Schema | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `name` | string | Supplier name | | `cui` | string \| null | Tax identification number (CUI) | | `email` | string \| null | Email address | | `phone` | string \| null | Phone number | | `address` | string \| null | Street address | | `city` | string \| null | City | | `county` | string \| null | County | | `country` | string | Country code | | `postalCode` | string \| null | Postal code | | `bankName` | string \| null | Bank name | | `bankAccount` | string \| null | Bank account (IBAN) | | `notes` | string \| null | Internal notes | | `invoiceSummary` | object | Summary of incoming invoice statistics | | `recentInvoices` | array | Array of recent incoming invoice objects (last 10) | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | ### Invoice Summary Object | Field | Type | Description | |-------|------|-------------| | `totalCount` | integer | Total number of incoming invoices | | `totalExpenses` | number | Total expenses from all invoices | | `averageInvoiceAmount` | number | Average invoice amount | | `lastInvoiceDate` | string \| null | ISO 8601 date of most recent invoice | ### Recent Invoice Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Invoice UUID | | `number` | string | Invoice number | | `issueDate` | string | ISO 8601 issue date | | `dueDate` | string \| null | ISO 8601 due date | | `totalAmount` | number | Total amount | | `currency` | string | Currency code | | `status` | string | Invoice status | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/suppliers/supplier-uuid-1' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json { "uuid": "supplier-uuid-1", "name": "Enel Energie Muntenia SA", "cui": "RO13267221", "email": "relatii.clienti@enel.ro", "phone": "+40214029700", "address": "Bd. Unirii, Nr. 74", "city": "București", "county": "București", "country": "RO", "postalCode": "030823", "bankName": "Banca Transilvania", "bankAccount": "RO49BTRL01304402S0390901", "notes": "Furnizor energie electrică - plată lunar prin debit direct", "invoiceSummary": { "totalCount": 12, "totalExpenses": 4580.50, "averageInvoiceAmount": 381.71, "lastInvoiceDate": "2026-02-10" }, "recentInvoices": [ { "uuid": "incoming-inv-uuid-1", "number": "2026/FAC00458", "issueDate": "2026-02-10", "dueDate": "2026-02-25", "totalAmount": 392.40, "currency": "RON", "status": "received" }, { "uuid": "incoming-inv-uuid-2", "number": "2026/FAC00234", "issueDate": "2026-01-10", "dueDate": "2026-01-25", "totalAmount": 385.20, "currency": "RON", "status": "received" }, { "uuid": "incoming-inv-uuid-3", "number": "2025/FAC04521", "issueDate": "2025-12-10", "dueDate": "2025-12-25", "totalAmount": 378.90, "currency": "RON", "status": "received" } ], "createdAt": "2025-06-01T08:00:00Z", "updatedAt": "2026-02-10T10:15:00Z" } ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Supplier not found or doesn't belong to company | ## Important Notes - Suppliers in Storno.ro are automatically created from incoming invoices via ANAF e-Factura - Supplier core data (name, CUI, address) comes from ANAF and cannot be manually edited - Only contact information and notes can be updated via the [update endpoint](/api-reference/suppliers/update) - Invoice summary statistics are calculated in real-time ## Related Endpoints - [List suppliers](/api-reference/suppliers/list) - [Update supplier](/api-reference/suppliers/update) - [Sync from ANAF](/api-reference/anaf/sync-invoices) --- ## List suppliers > Retrieve a paginated, alphabetically grouped list of suppliers. URL: https://docs.storno.ro/api-reference/suppliers/list # List suppliers Retrieves a paginated list of suppliers for the authenticated company, grouped alphabetically by the first letter of their name. Results can be searched by name or tax identification number. ```http GET /api/v1/suppliers ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `page` | integer | No | Page number (default: 1) | | `limit` | integer | No | Items per page (default: 50, max: 200) | | `search` | string | No | Search term to filter by name, CUI, or email | ## Response Returns a paginated object with suppliers grouped alphabetically. ### Response Schema | Field | Type | Description | |-------|------|-------------| | `data` | object | Object with alphabetic keys (A-Z, #) containing arrays of suppliers | | `total` | integer | Total number of suppliers matching filters | | `page` | integer | Current page number | | `limit` | integer | Items per page | | `pages` | integer | Total number of pages | ### Supplier Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `name` | string | Supplier name | | `cui` | string \| null | Tax identification number (CUI) | | `email` | string \| null | Email address | | `phone` | string \| null | Phone number | | `address` | string \| null | Street address | | `city` | string \| null | City | | `county` | string \| null | County | | `country` | string | Country code (default: "RO") | | `postalCode` | string \| null | Postal code | | `bankName` | string \| null | Bank name | | `bankAccount` | string \| null | Bank account (IBAN) | | `notes` | string \| null | Internal notes | | `invoiceCount` | integer | Number of incoming invoices from this supplier | | `totalExpenses` | number | Total expenses from this supplier | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/suppliers?page=1&limit=50' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json { "data": { "E": [ { "uuid": "supplier-uuid-1", "name": "Enel Energie Muntenia SA", "cui": "RO13267221", "email": "relatii.clienti@enel.ro", "phone": "+40214029700", "address": "Bd. Unirii, Nr. 74", "city": "București", "county": "București", "country": "RO", "postalCode": "030823", "bankName": "Banca Transilvania", "bankAccount": "RO49BTRL01304402S0390901", "notes": "Furnizor energie electrică", "invoiceCount": 12, "totalExpenses": 4580.50, "createdAt": "2025-06-01T08:00:00Z", "updatedAt": "2026-02-10T10:15:00Z" } ], "O": [ { "uuid": "supplier-uuid-2", "name": "Orange România SA", "cui": "RO10625813", "email": "office@orange.ro", "phone": "+40214037777", "address": "Str. Laminorului, Nr. 39", "city": "București", "county": "București", "country": "RO", "postalCode": "020251", "bankName": "BCR", "bankAccount": "RO49RNCB0082034687510001", "notes": "Servicii telefonie și internet", "invoiceCount": 8, "totalExpenses": 1240.00, "createdAt": "2025-06-15T09:30:00Z", "updatedAt": "2026-02-05T14:20:00Z" } ] }, "total": 23, "page": 1, "limit": 50, "pages": 1 } ``` ## Alphabetical Grouping Suppliers are grouped by the first character of their name: - Letters A-Z: Standard alphabetic grouping - "#": Used for names starting with numbers or special characters If a letter has no suppliers, it will not appear in the response object. ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 422 | validation_error | Invalid query parameters | ## Important Notes - Suppliers in Storno.ro are sync-only and come from incoming invoices via ANAF e-Factura - Supplier data is automatically created from incoming invoice metadata - Manual supplier updates are limited to contact information and notes - Use the [ANAF sync endpoint](/api-reference/anaf/sync-invoices) to fetch latest supplier data ## Related Endpoints - [Get supplier](/api-reference/suppliers/get) - [Update supplier](/api-reference/suppliers/update) - [Sync from ANAF](/api-reference/anaf/sync-invoices) --- ## Update supplier > Update editable fields of a supplier record. URL: https://docs.storno.ro/api-reference/suppliers/update # Update supplier Updates editable fields of a supplier record. Core supplier data (name, CUI) from ANAF cannot be modified, but contact information and notes can be updated. ```http PATCH /api/v1/suppliers/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the supplier | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters All parameters are optional, but at least one must be provided. | Parameter | Type | Description | |-----------|------|-------------| | `email` | string \| null | Email address | | `phone` | string \| null | Phone number | | `address` | string \| null | Street address | | `city` | string \| null | City | | `county` | string \| null | County | | `country` | string | Country code (ISO 3166-1 alpha-2) | | `postalCode` | string \| null | Postal code | | `bankName` | string \| null | Bank name | | `bankAccount` | string \| null | Bank account (IBAN) | | `notes` | string \| null | Internal notes | ## Response Returns the updated supplier object. ## Example Request ```bash curl -X PATCH 'https://api.storno.ro/api/v1/suppliers/supplier-uuid-1' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "email": "relatii.clienti.bucuresti@enel.ro", "phone": "+40214029701", "notes": "Furnizor energie electrică - plată lunar prin debit direct. Contact preferential: email" }' ``` ## Example Response ```json { "uuid": "supplier-uuid-1", "name": "Enel Energie Muntenia SA", "cui": "RO13267221", "email": "relatii.clienti.bucuresti@enel.ro", "phone": "+40214029701", "address": "Bd. Unirii, Nr. 74", "city": "București", "county": "București", "country": "RO", "postalCode": "030823", "bankName": "Banca Transilvania", "bankAccount": "RO49BTRL01304402S0390901", "notes": "Furnizor energie electrică - plată lunar prin debit direct. Contact preferential: email", "invoiceCount": 12, "totalExpenses": 4580.50, "createdAt": "2025-06-01T08:00:00Z", "updatedAt": "2026-02-16T14:20:00Z" } ``` ## Field Restrictions ### Non-Editable Fields The following fields come from ANAF and cannot be modified via API: - `name` - Supplier name - `cui` - Tax identification number These fields are automatically updated during ANAF synchronization. ### Editable Fields The following fields can be updated: - Contact information: `email`, `phone` - Address details: `address`, `city`, `county`, `country`, `postalCode` - Banking details: `bankName`, `bankAccount` - Internal data: `notes` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | Supplier not found or doesn't belong to company | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - No fields provided for update - Invalid email format - Invalid country code - Invalid IBAN format ## Important Notes - Supplier core data (name, CUI) is sync-only from ANAF - Use this endpoint to add or update internal contact information - Notes field is useful for tracking payment terms, contact preferences, and account details - Changes are immediately reflected in supplier details and reports ## Related Endpoints - [Get supplier](/api-reference/suppliers/get) - [Delete supplier](/api-reference/suppliers/delete) - [List suppliers](/api-reference/suppliers/list) --- ## System Version > Backend, web and mobile version metadata for client update prompts URL: https://docs.storno.ro/api-reference/system/version # System Version Public, unauthenticated endpoint that returns the running backend version plus the latest and minimum supported versions for each client surface. Used by the mobile app to prompt users when a newer build is available in their store, and by tooling that needs a quick liveness probe. --- ## Get Version ```http GET /api/v1/version ``` ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | platform | string | No | One of `ios`, `android`, `huawei`. When supplied, the response includes a flat `client` object with the matching mobile platform's metadata so callers don't need to branch. | | version | string | No | The current client version (e.g. `1.4.2`). Only honoured when `platform` is also set. The server uses it to compute an upgrade `tier` so the client doesn't have to compare versions itself. Build metadata (`+build.42`) is stripped before comparison. The same value can be supplied via the `X-App-Version` request header instead — the query param wins if both are present. | ### Response Version values are illustrative — call the endpoint for current values. ```json { "version": "", "web": { "latest": "", "min": "", "releaseNotes": "https://github.com/stornoro/storno/releases" }, "mobile": { "ios": { "latest": "", "min": "", "storeUrl": "https://apps.apple.com/app/storno-ro/id6761785908" }, "android": { "latest": "", "min": "", "storeUrl": "https://play.google.com/store/apps/details?id=com.storno.app" }, "huawei": { "latest": "", "min": "", "storeUrl": "https://appgallery.huawei.com/app/" } } } ``` When called with `?platform=ios` the response also includes: ```json { "platform": "ios", "client": { "latest": "", "min": "", "storeUrl": "https://apps.apple.com/app/storno-ro/id6761785908" } } ``` When called with both `?platform=ios&version=1.0.0` (or via the `X-App-Version` header), the response also includes a `gate` object with the resolved upgrade tier so the client does not need to compare versions: ```json { "gate": { "tier": "blocking", "min": "1.2.0", "latest": "1.5.0", "storeUrl": "https://apps.apple.com/app/storno-ro/id6761785908", "releaseNotesUrl": null, "message": { "ro": "Actualizare critica de securitate.", "en": "Critical security update." } } } ``` #### `gate.tier` values | Tier | Meaning | |------|---------| | `blocking` | Client `version` is below `min`. The client must render a non-dismissible "must update" screen and refuse to render the rest of the app until the user updates. The server **also enforces this** — see "Server enforcement" below. | | `recommended` | Client `version` is at or above `min` but below `latest`. The client should render a dismissible "update available" prompt. | | `ok` | Client is at or above `latest`. Render nothing. | | `unknown` | Platform was supplied but no `version` was passed. Render nothing. | ### Field reference | Field | Description | |-------|-------------| | `version` | Backend build read from `VERSION.txt`. | | `web.latest` | Latest web build (mirrors `version`). | | `web.min` | Lowest web version still supported by the API. | | `web.releaseNotes` | URL with the changelog for the web build. | | `mobile.{platform}.latest` | Latest binary published in that platform's store. | | `mobile.{platform}.min` | Lowest mobile version still allowed to call the API. Only bumped when a breaking server change ships. | | `mobile.{platform}.storeUrl` | Deep link the in-app prompt opens for the Update button. | ### Recommended client behavior - Cold-start and on app foreground, GET this endpoint with `?platform=` and the current client version (either as `?version=` or via the `X-App-Version` header). - Switch on `gate.tier`: - `blocking` → render a full-screen blocker. Only action is the Update button (opens `storeUrl`). Do not render the rest of the navigator. - `recommended` → render a dismissible modal with the Update button and `gate.message` (localised) underneath. Persist the dismissed `latest` so the prompt does not re-fire until a newer build is published. - `ok` / `unknown` → render nothing. ### Notification fan-out When the admin endpoint `PUT /api/v1/admin/version-overrides/{platform}` is called with `"notify": true` in the body, the server dispatches a `BroadcastVersionGateMessage` after persisting the override. The handler: 1. Queries telemetry for every user who has reported activity on the platform in the last 30 days (matched by `(user_id, platform, app_version)`). 2. For each user, resolves the tier against the new effective `min`/`latest`. 3. Creates an in-app `Notification` with a tier-appropriate title and body (uses the server-supplied `messageOverride[locale]` if present, else falls back to the bundled `notifications..yaml` strings). 4. The notification fans out automatically to Centrifugo (real-time bell refresh) and the push transport via the existing `SendExternalNotificationMessage` pipeline. Push delivery respects the user's `respectQuietHours` flag — except for `blocking` tier, which is delivered regardless. Pass `"notify": false` (or omit) to update the override silently (typos, rollbacks, no-op corrections). The flag is per-call, not persisted. ### Server enforcement A Symfony event subscriber rejects any incoming request from a mobile client whose reported version is below `min` with **HTTP `426 Upgrade Required`** and the same `tier` / `min` / `latest` / `storeUrl` / `message` payload as the gate object. This stops a stale client that ignores its in-app blocker from continuing to call the API. A request is gated when both `X-Platform` (one of `ios`, `android`, `huawei`) and `X-App-Version` are present. Web traffic, server-to-server calls and any client that does not set both headers pass through unchanged. The following paths are **always reachable**, even when the client is below `min`, so the user can still update + re-authenticate: - `GET /api/v1/version` — fetch the gate - `POST /api/auth` and `POST /api/auth/refresh` — log in / refresh - `POST /api/v1/telemetry` — phone home from a blocked build - `GET /api/v1/system/health` — health probes --- ## Create VAT rate > Add a new VAT rate to the company configuration. URL: https://docs.storno.ro/api-reference/vat-rates/create # Create VAT rate Creates a new VAT rate for the authenticated company. ```http POST /api/v1/vat-rates ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `rate` | number | Yes | VAT percentage (e.g., 19 for 19%) | | `label` | string | Yes | Display label (e.g., "TVA 19%") | | `categoryCode` | string | No | e-Factura category code (default: "S") | | `isDefault` | boolean | No | Set as default rate (default: false) | | `position` | integer | No | Display order (auto-assigned if not provided) | ## Response Returns the created VAT rate object with a `201 Created` status. ## Example Request ```bash curl -X POST 'https://api.storno.ro/api/v1/vat-rates' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "rate": 19, "label": "TVA 19%", "categoryCode": "S", "isDefault": true, "position": 1 }' ``` ## Example Response ```json { "uuid": "vat-uuid-1", "rate": 19, "label": "TVA 19%", "categoryCode": "S", "isDefault": true, "position": 1, "createdAt": "2026-02-16T16:30:00Z", "updatedAt": "2026-02-16T16:30:00Z" } ``` ## Category Codes Common e-Factura category codes: | Code | Description | Typical Use | |------|-------------|-------------| | `S` | Standard rate | Default for most goods/services | | `AA` | Reduced rate | Lower VAT rates (9%, 5%) | | `E` | Exempt | VAT-exempt transactions | | `O` | Outside scope | Not subject to VAT | | `Z` | Zero rated | 0% VAT but with input deduction | | `AE` | Reverse charge | VAT responsibility on buyer | ## Default Rate Behavior - If `isDefault` is `true`, any existing default rate will be set to non-default - Only one VAT rate can be marked as default - The default rate is automatically selected in new invoice lines - If this is the first VAT rate, it automatically becomes the default ## Position Assignment - If `position` is not provided, the system assigns the next available position - Lower position numbers appear first in dropdowns - You can specify any integer to control ordering ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - Missing `rate` field - Missing `label` field - Negative rate value - Invalid category code - Duplicate rate and label combination ## Related Endpoints - [List VAT rates](/api-reference/vat-rates/list) - [Update VAT rate](/api-reference/vat-rates/update) - [Delete VAT rate](/api-reference/vat-rates/delete) --- ## Delete VAT rate > Soft-delete a VAT rate from the company configuration. URL: https://docs.storno.ro/api-reference/vat-rates/delete # Delete VAT rate Soft-deletes a VAT rate from the authenticated company. The rate is marked as deleted but not permanently removed, preserving historical invoice data integrity. ```http DELETE /api/v1/vat-rates/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the VAT rate | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns a `204 No Content` status on successful deletion with no response body. ## Example Request ```bash curl -X DELETE 'https://api.storno.ro/api/v1/vat-rates/vat-uuid-4' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```http HTTP/1.1 204 No Content ``` ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | VAT rate not found or doesn't belong to company | | 409 | conflict | Cannot delete the default VAT rate or last remaining rate | ## Soft Delete Behavior This endpoint performs a soft delete: - The VAT rate is marked as deleted (`deletedAt` timestamp is set) - The rate no longer appears in list endpoints or dropdowns - Existing invoice lines that use this rate are not affected - Historical reports continue to display the rate correctly - The rate can be restored by support if needed ## Important Notes - You cannot delete the default VAT rate; set another rate as default first - You cannot delete the last remaining VAT rate; create another rate first - Existing invoices preserve the VAT rate at the time of creation - Invoice line items retain the full VAT rate details (percentage, label, category) ## Recommended Approach Before deleting a VAT rate: 1. **Check usage** - Verify how many invoices use this rate 2. **Set new default** - If deleting the default rate, set another as default 3. **Create replacement** - Ensure an alternative rate exists for future invoices 4. **Document reason** - Keep internal notes on why the rate was removed ## Related Endpoints - [List VAT rates](/api-reference/vat-rates/list) - [Create VAT rate](/api-reference/vat-rates/create) - [Update VAT rate](/api-reference/vat-rates/update) --- ## List VAT rates > Retrieve all VAT rates configured for the authenticated company. URL: https://docs.storno.ro/api-reference/vat-rates/list # List VAT rates Retrieves all VAT rates configured for the authenticated company, sorted by position. ```http GET /api/v1/vat-rates ``` ## Request ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | ## Response Returns an array of VAT rate objects sorted by position. ### VAT Rate Object | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier | | `rate` | number | VAT percentage (e.g., 19 for 19%) | | `label` | string | Display label (e.g., "TVA 19%", "Scutit") | | `categoryCode` | string | e-Factura category code (default: "S") | | `isDefault` | boolean | Whether this is the default rate | | `position` | integer | Display order (lower numbers appear first) | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | ## Example Request ```bash curl -X GET 'https://api.storno.ro/api/v1/vat-rates' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' ``` ## Example Response ```json [ { "uuid": "vat-uuid-1", "rate": 19, "label": "TVA 19%", "categoryCode": "S", "isDefault": true, "position": 1, "createdAt": "2025-06-01T10:00:00Z", "updatedAt": "2025-06-01T10:00:00Z" }, { "uuid": "vat-uuid-2", "rate": 9, "label": "TVA 9%", "categoryCode": "AA", "isDefault": false, "position": 2, "createdAt": "2025-06-01T10:05:00Z", "updatedAt": "2025-06-01T10:05:00Z" }, { "uuid": "vat-uuid-3", "rate": 5, "label": "TVA 5%", "categoryCode": "AA", "isDefault": false, "position": 3, "createdAt": "2025-06-01T10:10:00Z", "updatedAt": "2025-06-01T10:10:00Z" }, { "uuid": "vat-uuid-4", "rate": 0, "label": "Scutit", "categoryCode": "E", "isDefault": false, "position": 4, "createdAt": "2025-06-01T10:15:00Z", "updatedAt": "2025-06-01T10:15:00Z" } ] ``` ## Common VAT Rates in Romania | Rate | Label | Category Code | Use Case | |------|-------|---------------|----------| | 19% | TVA 19% | S | Standard rate (most goods and services) | | 9% | TVA 9% | AA | Reduced rate (food, medicines, hotels, restaurants) | | 5% | TVA 5% | AA | Super-reduced rate (books, newspapers) | | 0% | Scutit | E | Exempt (education, healthcare, financial services) | | 0% | Neimpozabil | O | Outside scope (intra-community, exports) | ## e-Factura Category Codes - `S` - Standard rate - `AA` - Reduced rate - `E` - Exempt - `O` - Outside scope (not subject to VAT) - `Z` - Zero rated - `AE` - Reverse charge ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | ## Important Notes - Each company should configure the VAT rates they use - Only one rate can be marked as default - The default rate is automatically selected when creating new invoice lines - Position determines the order in dropdowns and forms - Deleting a VAT rate is a soft delete - rates used in invoices are preserved ## Related Endpoints - [Create VAT rate](/api-reference/vat-rates/create) - [Update VAT rate](/api-reference/vat-rates/update) - [Delete VAT rate](/api-reference/vat-rates/delete) --- ## Update VAT rate > Update an existing VAT rate configuration. URL: https://docs.storno.ro/api-reference/vat-rates/update # Update VAT rate Updates an existing VAT rate for the authenticated company. ```http PATCH /api/v1/vat-rates/{uuid} ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `uuid` | string | Yes | The UUID of the VAT rate | ### Headers | Header | Type | Required | Description | |--------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | UUID of the company context | | `Content-Type` | string | Yes | Must be `application/json` | ### Body Parameters All parameters are optional, but at least one must be provided. | Parameter | Type | Description | |-----------|------|-------------| | `rate` | number | VAT percentage | | `label` | string | Display label | | `categoryCode` | string | e-Factura category code | | `isDefault` | boolean | Set as default rate | | `position` | integer | Display order | ## Response Returns the updated VAT rate object. ## Example Request ```bash curl -X PATCH 'https://api.storno.ro/api/v1/vat-rates/vat-uuid-2' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'X-Company: company-uuid' \ -H 'Content-Type: application/json' \ -d '{ "label": "TVA redusă 9%", "position": 2 }' ``` ## Example Response ```json { "uuid": "vat-uuid-2", "rate": 9, "label": "TVA redusă 9%", "categoryCode": "AA", "isDefault": false, "position": 2, "createdAt": "2025-06-01T10:05:00Z", "updatedAt": "2026-02-16T16:45:00Z" } ``` ## Default Rate Behavior - If `isDefault` is set to `true`, any existing default rate will be set to non-default - Only one VAT rate can be marked as default - Setting `isDefault` to `false` on the default rate requires another rate to be set as default first ## Important Notes - Changing the rate percentage does not retroactively affect existing invoices - Existing invoice lines preserve the VAT rate that was used at creation time - Consider creating a new VAT rate instead of modifying an existing one if the rate changes - Updating the label only affects future displays; existing invoices store the label at creation ## Errors | Status Code | Error Code | Description | |-------------|------------|-------------| | 401 | unauthorized | Invalid or missing authentication token | | 403 | forbidden | Missing or invalid X-Company header | | 404 | not_found | VAT rate not found or doesn't belong to company | | 422 | validation_error | Invalid input data (see error details) | ### Validation Errors Common validation errors include: - No fields provided for update - Negative rate value - Invalid category code - Duplicate rate and label combination ## Related Endpoints - [List VAT rates](/api-reference/vat-rates/list) - [Create VAT rate](/api-reference/vat-rates/create) - [Delete VAT rate](/api-reference/vat-rates/delete) --- ## Create webhook > Register a new webhook endpoint for the current company URL: https://docs.storno.ro/api-reference/webhooks/create-webhook # Create webhook Registers a new webhook endpoint for the company identified by the `X-Company` header. The response includes the full signing secret — store it securely immediately, as it will be masked in all subsequent responses. ``` POST /api/v1/webhooks ``` The `secret` field is returned in full only on creation. Copy it to a secure location before leaving this page — subsequent GET requests will return a masked value. If you lose the secret, use the [regenerate-secret endpoint](/api-reference/webhooks/regenerate-secret) to issue a new one. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Request body | Name | Type | Required | Description | |------|------|----------|-------------| | `url` | string | Yes | HTTPS destination URL that will receive POST requests | | `events` | array | Yes | Array of event type names to subscribe to (see [List event types](/api-reference/webhooks/list-events)) | | `description` | string | No | Human-readable label for this webhook endpoint | | `isActive` | boolean | No | Whether the webhook is active on creation (default: `true`) | The `url` must use HTTPS. HTTP URLs are rejected. Use the wildcard value `["*"]` for `events` to subscribe to all current and future event types. ## Request ```bash curl -X POST https://api.storno.ro/api/v1/webhooks \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.example.com/webhooks/storno", "events": ["invoice.created", "invoice.validated", "invoice.rejected", "invoice.paid"], "description": "Production invoice notifications", "isActive": true }' ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/webhooks', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000', 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://your-app.example.com/webhooks/storno', events: ['invoice.created', 'invoice.validated', 'invoice.rejected', 'invoice.paid'], description: 'Production invoice notifications', isActive: true }) }); // Returns 201 Created const webhook = await response.json(); // Store webhook.secret securely — it will be masked in future responses console.log('Signing secret:', webhook.secret); ``` ## Response Returns the created webhook object with status `201 Created`. The `secret` field contains the full HMAC-SHA256 signing key. ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "url": "https://your-app.example.com/webhooks/storno", "description": "Production invoice notifications", "events": [ "invoice.created", "invoice.validated", "invoice.rejected", "invoice.paid" ], "isActive": true, "secret": "whsec_9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "createdAt": "2026-02-18T10:00:00Z", "updatedAt": "2026-02-18T10:00:00Z" } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier for the new webhook endpoint | | `url` | string | The registered destination URL | | `description` | string | The provided description label | | `events` | array | List of subscribed event type names | | `isActive` | boolean | Active status of the webhook | | `secret` | string | Full HMAC-SHA256 signing secret — save this value immediately | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 last-updated timestamp | ### Verifying webhook signatures Each delivery includes an `X-Storno-Signature` header containing a HMAC-SHA256 hex digest of the raw request body signed with the secret. Verify it server-side: ```javascript const crypto = require('crypto'); function verifySignature(rawBody, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } ``` ## Error codes | Code | Description | |------|-------------| | `400` | Validation error — missing required fields or invalid data | | `401` | Missing or invalid authentication token | | `403` | Insufficient permissions — requires `webhook.manage` permission | | `422` | Business validation error — URL must use HTTPS, or unknown event type specified | --- ## Delete webhook > Permanently delete a webhook endpoint and all its delivery history URL: https://docs.storno.ro/api-reference/webhooks/delete-webhook # Delete webhook Permanently deletes a webhook endpoint and all associated delivery records. This is a hard delete — there is no soft-delete or recovery mechanism. ``` DELETE /api/v1/webhooks/{uuid} ``` This action is permanent and cannot be undone. All delivery history for this endpoint will also be deleted. If you only want to stop receiving deliveries temporarily, set `isActive` to `false` via the [update endpoint](/api-reference/webhooks/update-webhook) instead. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Webhook endpoint UUID | ## Request ```bash curl -X DELETE https://api.storno.ro/api/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; const response = await fetch(`https://api.storno.ro/api/v1/webhooks/${uuid}`, { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); // Returns 204 No Content on success if (response.status === 204) { console.log('Webhook deleted successfully'); } ``` ## Response Returns `204 No Content` on successful deletion with an empty response body. ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | Insufficient permissions — requires `webhook.manage` permission | | `404` | Webhook endpoint not found for this company | ## Related endpoints - [List webhooks](/api-reference/webhooks/list-webhooks) — List all remaining webhook endpoints - [Update webhook](/api-reference/webhooks/update-webhook) — Pause deliveries without deleting --- ## Get webhook > Retrieve details of a single webhook endpoint URL: https://docs.storno.ro/api-reference/webhooks/get-webhook # Get webhook Returns the full configuration of a single webhook endpoint belonging to the current company. The `secret` field is always masked in this response. Use the [regenerate-secret endpoint](/api-reference/webhooks/regenerate-secret) if you need to obtain a new signing secret. ``` GET /api/v1/webhooks/{uuid} ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Webhook endpoint UUID | ## Request ```bash curl https://api.storno.ro/api/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; const response = await fetch(`https://api.storno.ro/api/v1/webhooks/${uuid}`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const webhook = await response.json(); ``` ## Response Returns the webhook endpoint object with a masked secret. ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "url": "https://your-app.example.com/webhooks/storno", "description": "Production invoice notifications", "events": [ "invoice.created", "invoice.validated", "invoice.rejected", "invoice.paid" ], "isActive": true, "secret": "whsec_••••••••••••••••••••••••", "deliveriesCount": 142, "lastDeliveryAt": "2026-02-18T09:45:00Z", "lastDeliveryStatus": "success", "createdAt": "2026-02-10T09:00:00Z", "updatedAt": "2026-02-15T14:30:00Z" } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier for the webhook endpoint | | `url` | string | The HTTPS destination URL | | `description` | string | Human-readable label | | `events` | array | List of subscribed event type names | | `isActive` | boolean | Whether the webhook is receiving deliveries | | `secret` | string | Masked signing secret (format: `whsec_••••••••`) | | `deliveriesCount` | integer | Total number of delivery attempts made for this webhook | | `lastDeliveryAt` | string | ISO 8601 timestamp of the most recent delivery attempt | | `lastDeliveryStatus` | string | Outcome of the last delivery: `success`, `failed`, or `pending` | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 last-updated timestamp | ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | Insufficient permissions — requires `webhook.view` permission | | `404` | Webhook endpoint not found for this company | --- ## Get webhook delivery > Retrieve full details of a single webhook delivery attempt including request and response payloads URL: https://docs.storno.ro/api-reference/webhooks/get-delivery # Get webhook delivery Returns the complete details of a single delivery attempt, including the full request payload sent to your endpoint, the request headers (including the signature), and the full response received. Use this to debug failed deliveries or audit successful ones. ``` GET /api/v1/webhooks/{uuid}/deliveries/{deliveryUuid} ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Webhook endpoint UUID | | `deliveryUuid` | string | Yes | Delivery attempt UUID | ## Request ```bash curl "https://api.storno.ro/api/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/deliveries/c3d4e5f6-a7b8-9012-cdef-123456789012" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const webhookUuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; const deliveryUuid = 'c3d4e5f6-a7b8-9012-cdef-123456789012'; const response = await fetch( `https://api.storno.ro/api/v1/webhooks/${webhookUuid}/deliveries/${deliveryUuid}`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } } ); const delivery = await response.json(); ``` ## Response Returns the complete delivery record including request and response details. ```json { "uuid": "c3d4e5f6-a7b8-9012-cdef-123456789012", "webhookUuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "eventType": "invoice.validated", "status": "failed", "attempt": 1, "requestUrl": "https://your-app.example.com/webhooks/storno", "requestMethod": "POST", "requestHeaders": { "Content-Type": "application/json", "User-Agent": "Storno-Webhooks/1.0", "X-Storno-Event": "invoice.validated", "X-Storno-Delivery": "c3d4e5f6-a7b8-9012-cdef-123456789012", "X-Storno-Signature": "sha256=3e7b3a4f8c2d5e1b9a0c6f4d2e8a5b1c3d7a0e4f8b2c5d9a3e6f1b4c8d2a5e9f" }, "requestPayload": { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "type": "invoice.validated", "createdAt": "2026-02-18T09:45:00Z", "company": "550e8400-e29b-41d4-a716-446655440000", "data": { "invoiceUuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "invoiceNumber": "FAC-2026-042", "status": "validated", "anafSubmissionId": "ANAF-2026-987654", "validatedAt": "2026-02-18T09:44:00Z" } }, "responseCode": 503, "responseHeaders": { "Content-Type": "text/plain", "Retry-After": "30" }, "responseBody": "Service Unavailable", "durationMs": 5002, "errorMessage": "Endpoint returned non-2xx status: 503", "deliveredAt": "2026-02-18T09:45:00Z", "nextRetryAt": null } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique delivery attempt identifier | | `webhookUuid` | string | UUID of the parent webhook endpoint | | `eventType` | string | The event type that triggered this delivery | | `status` | string | Outcome: `success` (2xx response) or `failed` (timeout or non-2xx) | | `attempt` | integer | Attempt number (1 for initial, higher for retries) | | `requestUrl` | string | The URL the delivery was posted to | | `requestMethod` | string | Always `POST` | | `requestHeaders` | object | HTTP headers sent with the delivery request | | `requestPayload` | object | The exact JSON body sent to your endpoint | | `responseCode` | integer | HTTP status code returned by your endpoint, or `0` on timeout | | `responseHeaders` | object | HTTP response headers returned by your endpoint | | `responseBody` | string | First 4,096 characters of the response body returned by your endpoint | | `durationMs` | integer | Round-trip time in milliseconds | | `errorMessage` | string | Human-readable failure reason, or `null` on success | | `deliveredAt` | string | ISO 8601 timestamp when the delivery was attempted | | `nextRetryAt` | string | ISO 8601 timestamp of the next scheduled retry, or `null` if no retry is pending | ### Delivery payload structure The `requestPayload.data` object shape varies by event type: | Event type | `data` content | |------------|----------------| | `invoice.created` | `invoiceUuid`, `invoiceNumber`, `status`, `direction`, `clientName`, `total`, `currency` | | `invoice.validated` | `invoiceUuid`, `invoiceNumber`, `status`, `anafSubmissionId`, `validatedAt` | | `invoice.rejected` | `invoiceUuid`, `invoiceNumber`, `status`, `anafSubmissionId`, `rejectionErrors` | | `invoice.paid` | `invoiceUuid`, `invoiceNumber`, `amountPaid`, `balance`, `paidAt` | | `sync.completed` | `syncId`, `invoicesSynced`, `duration`, `completedAt` | | `sync.failed` | `syncId`, `errorMessage`, `failedAt` | | `webhook.test` | `message`, `webhookUuid` | ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | Insufficient permissions — requires `webhook.view` permission | | `404` | Webhook endpoint or delivery record not found for this company | ## Related endpoints - [List deliveries](/api-reference/webhooks/list-deliveries) — Browse all delivery attempts with filtering - [Test webhook](/api-reference/webhooks/test-webhook) — Send a new test delivery to inspect the full flow --- ## List webhook deliveries > Retrieve a paginated list of delivery attempts for a webhook endpoint URL: https://docs.storno.ro/api-reference/webhooks/list-deliveries # List webhook deliveries Returns a paginated list of delivery attempts for the specified webhook endpoint. Each record summarizes the event type, outcome, response code, and timing of a single delivery attempt. Use the [get delivery endpoint](/api-reference/webhooks/get-delivery) to inspect the full request and response payload for any entry. ``` GET /api/v1/webhooks/{uuid}/deliveries ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Webhook endpoint UUID | ## Query parameters | Name | Type | Default | Description | |------|------|---------|-------------| | `page` | integer | 1 | Page number for pagination | | `limit` | integer | 20 | Number of items per page (max 100) | | `status` | string | - | Filter by delivery status: `success` or `failed` | | `eventType` | string | - | Filter by event type name (e.g., `invoice.validated`) | | `from` | string | - | Start date filter (ISO 8601 format: YYYY-MM-DD) | | `to` | string | - | End date filter (ISO 8601 format: YYYY-MM-DD) | ## Request ```bash curl "https://api.storno.ro/api/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/deliveries?page=1&limit=20&status=failed" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; const params = new URLSearchParams({ page: 1, limit: 20, status: 'failed' }); const response = await fetch( `https://api.storno.ro/api/v1/webhooks/${uuid}/deliveries?${params}`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } } ); const data = await response.json(); ``` ## Response Returns a paginated list of delivery attempt summaries. ```json { "data": [ { "uuid": "c3d4e5f6-a7b8-9012-cdef-123456789012", "eventType": "invoice.validated", "status": "failed", "responseCode": 503, "durationMs": 5002, "deliveredAt": "2026-02-18T09:45:00Z", "attempt": 1 }, { "uuid": "d4e5f6a7-b8c9-0123-def0-234567890123", "eventType": "invoice.created", "status": "success", "responseCode": 200, "durationMs": 87, "deliveredAt": "2026-02-18T08:30:00Z", "attempt": 1 }, { "uuid": "e5f6a7b8-c9d0-1234-ef01-345678901234", "eventType": "invoice.paid", "status": "success", "responseCode": 204, "durationMs": 112, "deliveredAt": "2026-02-17T16:00:00Z", "attempt": 1 } ], "total": 142, "page": 1, "limit": 20, "pages": 8 } ``` ### Pagination fields | Field | Type | Description | |-------|------|-------------| | `data` | array | Array of delivery summary objects | | `total` | integer | Total number of delivery attempts matching the filters | | `page` | integer | Current page number | | `limit` | integer | Items per page | | `pages` | integer | Total number of pages | ### Delivery summary fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique delivery attempt identifier | | `eventType` | string | The event type that triggered this delivery | | `status` | string | Outcome: `success` (2xx response) or `failed` (timeout or non-2xx) | | `responseCode` | integer | HTTP status code returned by your endpoint, or `0` on timeout | | `durationMs` | integer | Round-trip time in milliseconds | | `deliveredAt` | string | ISO 8601 timestamp when the delivery was attempted | | `attempt` | integer | Attempt number (1 for initial, higher for retries) | ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | Insufficient permissions — requires `webhook.view` permission | | `404` | Webhook endpoint not found for this company | | `422` | Invalid query parameter value | ## Related endpoints - [Get delivery](/api-reference/webhooks/get-delivery) — Inspect the full payload and response for a single delivery - [Test webhook](/api-reference/webhooks/test-webhook) — Trigger a new test delivery --- ## List webhook event types > Retrieve all available webhook event types that can be subscribed to URL: https://docs.storno.ro/api-reference/webhooks/list-events # List webhook event types Returns an array of all supported webhook event types available for subscription. Use this endpoint to discover which events can be configured on a webhook endpoint. Authentication requires a valid JWT token; no company scope is needed. ``` GET /api/v1/webhooks/events ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | ## Request ```bash curl https://api.storno.ro/api/v1/webhooks/events \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/webhooks/events', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN' } }); const events = await response.json(); ``` ## Response Returns an array of event type objects describing each available event. ```json [ { "name": "invoice.created", "description": "Triggered when a new invoice is created, including those synced from ANAF", "category": "invoices" }, { "name": "invoice.updated", "description": "Triggered when an invoice is updated (status change, data edit, payment recorded)", "category": "invoices" }, { "name": "invoice.issued", "description": "Triggered when an invoice transitions to the issued state", "category": "invoices" }, { "name": "invoice.submitted", "description": "Triggered when an invoice is submitted to ANAF e-Factura", "category": "invoices" }, { "name": "invoice.validated", "description": "Triggered when ANAF validates an outgoing invoice", "category": "invoices" }, { "name": "invoice.rejected", "description": "Triggered when ANAF rejects an outgoing invoice", "category": "invoices" }, { "name": "invoice.cancelled", "description": "Triggered when an invoice is cancelled", "category": "invoices" }, { "name": "invoice.paid", "description": "Triggered when a payment is recorded and the invoice is fully paid", "category": "invoices" }, { "name": "payment.created", "description": "Triggered when a payment record is created on an invoice", "category": "payments" }, { "name": "payment.deleted", "description": "Triggered when a payment record is removed from an invoice", "category": "payments" }, { "name": "client.created", "description": "Triggered when a new client is added to the company", "category": "clients" }, { "name": "client.updated", "description": "Triggered when a client record is updated", "category": "clients" }, { "name": "sync.completed", "description": "Triggered when an ANAF sync run finishes successfully", "category": "sync" }, { "name": "sync.failed", "description": "Triggered when an ANAF sync run encounters a fatal error", "category": "sync" }, { "name": "proforma.created", "description": "Triggered when a new proforma invoice is created", "category": "proforma" }, { "name": "proforma.accepted", "description": "Triggered when a proforma invoice is accepted by the client", "category": "proforma" }, { "name": "proforma.rejected", "description": "Triggered when a proforma invoice is rejected by the client", "category": "proforma" } ] ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `name` | string | Unique event type identifier used when subscribing to events | | `description` | string | Human-readable description of when the event fires | | `category` | string | Logical grouping of the event (invoices, payments, clients, sync, proforma) | ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | --- ## List webhooks > Retrieve all webhook endpoints configured for the current company URL: https://docs.storno.ro/api-reference/webhooks/list-webhooks # List webhooks Returns an array of all webhook endpoints registered for the company identified by the `X-Company` header. Secrets are masked in this listing — retrieve a single webhook to see the masked secret or regenerate to obtain a new full secret. ``` GET /api/v1/webhooks ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Request ```bash curl https://api.storno.ro/api/v1/webhooks \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const response = await fetch('https://api.storno.ro/api/v1/webhooks', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const webhooks = await response.json(); ``` ## Response Returns an array of webhook endpoint objects. ```json [ { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "url": "https://your-app.example.com/webhooks/storno", "description": "Production invoice notifications", "events": [ "invoice.created", "invoice.validated", "invoice.rejected", "invoice.paid" ], "isActive": true, "secret": "whsec_••••••••••••••••••••••••", "createdAt": "2026-02-10T09:00:00Z", "updatedAt": "2026-02-15T14:30:00Z" }, { "uuid": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "url": "https://your-app.example.com/webhooks/sync", "description": "ANAF sync monitoring", "events": [ "sync.completed", "sync.failed" ], "isActive": false, "secret": "whsec_••••••••••••••••••••••••", "createdAt": "2026-01-20T11:15:00Z", "updatedAt": "2026-01-20T11:15:00Z" } ] ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier for the webhook endpoint | | `url` | string | The HTTPS URL that receives webhook POST requests | | `description` | string | Optional human-readable label for this webhook | | `events` | array | List of event type names this endpoint is subscribed to | | `isActive` | boolean | Whether the webhook will receive deliveries | | `secret` | string | Masked signing secret; shown in full only on creation or regeneration | | `createdAt` | string | ISO 8601 timestamp when the webhook was created | | `updatedAt` | string | ISO 8601 timestamp of the last update | ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | Insufficient permissions — requires `webhook.view` permission | --- ## Regenerate webhook secret > Issue a new signing secret for a webhook endpoint, immediately invalidating the previous one URL: https://docs.storno.ro/api-reference/webhooks/regenerate-secret # Regenerate webhook secret Issues a new HMAC-SHA256 signing secret for the specified webhook endpoint. The previous secret is immediately invalidated — any deliveries that arrive after this call will be signed with the new secret. Update your endpoint's verification logic before calling this in production. ``` POST /api/v1/webhooks/{uuid}/regenerate-secret ``` The new secret is returned in full only in this response. Store it securely immediately. All subsequent GET requests will return the masked value. If the endpoint is active, deliveries sent after this call will carry a signature computed with the new secret — update your verification logic first. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Webhook endpoint UUID | ## Request This endpoint requires no request body. ```bash curl -X POST https://api.storno.ro/api/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/regenerate-secret \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; const response = await fetch( `https://api.storno.ro/api/v1/webhooks/${uuid}/regenerate-secret`, { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } } ); const result = await response.json(); // Store the new secret securely — future responses will mask it console.log('New signing secret:', result.secret); ``` ## Response Returns the full webhook object with the new unmasked signing secret. ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "url": "https://your-app.example.com/webhooks/storno", "description": "Production invoice notifications", "events": [ "invoice.created", "invoice.validated", "invoice.rejected", "invoice.paid" ], "isActive": true, "secret": "whsec_c2b04e8f3d5a6e1f9b0c4d7a2e8f5b1c3d6a9e2f5b8c1d4a7e0f3b6c9d2a5e8f", "createdAt": "2026-02-10T09:00:00Z", "updatedAt": "2026-02-18T10:30:00Z" } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Webhook endpoint UUID | | `url` | string | Destination URL (unchanged) | | `description` | string | Description label (unchanged) | | `events` | array | Subscribed event types (unchanged) | | `isActive` | boolean | Active state (unchanged) | | `secret` | string | The new full HMAC-SHA256 signing secret — save this value immediately | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 timestamp of this regeneration | ### Rotation procedure To safely rotate a signing secret without dropping deliveries: 1. Call this endpoint to obtain the new secret. 2. Update your server to accept signatures from **both** the old and new secrets temporarily. 3. Verify that recent deliveries are arriving with the new signature. 4. Remove the old secret from your server's verification logic. ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | Insufficient permissions — requires `webhook.manage` permission | | `404` | Webhook endpoint not found for this company | ## Related endpoints - [Get webhook](/api-reference/webhooks/get-webhook) — View current webhook configuration - [Test webhook](/api-reference/webhooks/test-webhook) — Verify your endpoint handles the new secret correctly --- ## Test webhook > Send a synchronous test delivery to a webhook endpoint URL: https://docs.storno.ro/api-reference/webhooks/test-webhook # Test webhook Sends a synchronous test event delivery to the specified webhook endpoint and returns the outcome immediately. Use this to verify your endpoint URL is reachable, your signature verification logic is correct, and your server responds with a `2xx` status. ``` POST /api/v1/webhooks/{uuid}/test ``` The test delivery uses a synthetic payload with event type `webhook.test`. Your endpoint must respond within 10 seconds with any `2xx` HTTP status code for the test to be considered successful. The delivery is recorded in the webhook's delivery history. ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | No | Optionally `application/json` if providing a body | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Webhook endpoint UUID | ## Request body The request body is optional. If omitted, a default test payload is used. | Name | Type | Required | Description | |------|------|----------|-------------| | `eventType` | string | No | Override the test event type name (default: `webhook.test`) | ## Request ```bash curl -X POST https://api.storno.ro/api/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890/test \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" ``` ```javascript const uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; const response = await fetch(`https://api.storno.ro/api/v1/webhooks/${uuid}/test`, { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000' } }); const result = await response.json(); console.log('Test result:', result.status, result.responseCode); ``` ## Response Returns the outcome of the synchronous test delivery with status `200 OK`. ```json { "deliveryUuid": "c3d4e5f6-a7b8-9012-cdef-123456789012", "status": "success", "eventType": "webhook.test", "requestPayload": { "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "type": "webhook.test", "createdAt": "2026-02-18T10:00:00Z", "company": "550e8400-e29b-41d4-a716-446655440000", "data": { "message": "This is a test delivery from Storno.ro", "webhookUuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } }, "requestHeaders": { "Content-Type": "application/json", "X-Storno-Event": "webhook.test", "X-Storno-Signature": "sha256=3e7b3a...", "X-Storno-Delivery": "c3d4e5f6-a7b8-9012-cdef-123456789012" }, "responseCode": 200, "responseBody": "OK", "durationMs": 143, "deliveredAt": "2026-02-18T10:00:00Z" } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `deliveryUuid` | string | UUID of the delivery record created for this test | | `status` | string | Outcome: `success` (2xx response received) or `failed` (timeout or non-2xx) | | `eventType` | string | The event type name used for the test payload | | `requestPayload` | object | The exact JSON body sent to your endpoint | | `requestHeaders` | object | HTTP headers included in the delivery request | | `responseCode` | integer | HTTP status code returned by your endpoint | | `responseBody` | string | First 1,024 characters of your endpoint's response body | | `durationMs` | integer | Round-trip time in milliseconds | | `deliveredAt` | string | ISO 8601 timestamp of the delivery attempt | ### Delivery payload structure Every webhook delivery (including test deliveries) sends a JSON body with this shape: ```json { "id": "", "type": "", "createdAt": "", "company": "", "data": { } } ``` The `X-Storno-Signature` header contains `sha256=` where the HMAC is computed over the raw request body using your webhook's signing secret. ## Error codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | Insufficient permissions — requires `webhook.manage` permission | | `404` | Webhook endpoint not found for this company | | `422` | Webhook is inactive — activate it before sending a test delivery | --- ## Update webhook > Update the URL, events, description, or active state of an existing webhook endpoint URL: https://docs.storno.ro/api-reference/webhooks/update-webhook # Update webhook Partially updates an existing webhook endpoint for the current company. Only the fields provided in the request body are changed — omitted fields retain their current values. ``` PATCH /api/v1/webhooks/{uuid} ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token for authentication | | `X-Company` | string | Yes | Company UUID to scope the request | | `Content-Type` | string | Yes | Must be `application/json` | ## Path parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `uuid` | string | Yes | Webhook endpoint UUID | ## Request body All fields are optional. At least one field must be provided. | Name | Type | Required | Description | |------|------|----------|-------------| | `url` | string | No | New HTTPS destination URL | | `events` | array | No | Replacement list of event type names to subscribe to | | `description` | string | No | Updated human-readable label | | `isActive` | boolean | No | Enable (`true`) or pause (`false`) deliveries | Providing `events` replaces the entire subscription list — it is not additive. Send the complete desired set of event types each time. ## Request ```bash curl -X PATCH https://api.storno.ro/api/v1/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "X-Company: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{ "events": ["invoice.created", "invoice.validated", "invoice.rejected", "invoice.paid", "sync.completed"], "description": "Production notifications — expanded", "isActive": true }' ``` ```javascript const uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; const response = await fetch(`https://api.storno.ro/api/v1/webhooks/${uuid}`, { method: 'PATCH', headers: { 'Authorization': 'Bearer YOUR_TOKEN', 'X-Company': '550e8400-e29b-41d4-a716-446655440000', 'Content-Type': 'application/json' }, body: JSON.stringify({ events: ['invoice.created', 'invoice.validated', 'invoice.rejected', 'invoice.paid', 'sync.completed'], description: 'Production notifications — expanded', isActive: true }) }); const webhook = await response.json(); ``` ## Response Returns the updated webhook endpoint object. ```json { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "url": "https://your-app.example.com/webhooks/storno", "description": "Production notifications — expanded", "events": [ "invoice.created", "invoice.validated", "invoice.rejected", "invoice.paid", "sync.completed" ], "isActive": true, "secret": "whsec_••••••••••••••••••••••••", "createdAt": "2026-02-10T09:00:00Z", "updatedAt": "2026-02-18T10:15:00Z" } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `uuid` | string | Unique identifier for the webhook endpoint | | `url` | string | Current destination URL | | `description` | string | Current description label | | `events` | array | Current list of subscribed event type names | | `isActive` | boolean | Current active state | | `secret` | string | Masked signing secret (unchanged by this operation) | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 timestamp of this update | ## Error codes | Code | Description | |------|-------------| | `400` | Validation error — no fields provided or invalid data format | | `401` | Missing or invalid authentication token | | `403` | Insufficient permissions — requires `webhook.manage` permission | | `404` | Webhook endpoint not found for this company | | `422` | Business validation error — URL must use HTTPS, or unknown event type specified | ## Related endpoints - [Get webhook](/api-reference/webhooks/get-webhook) — Retrieve current webhook configuration - [Regenerate secret](/api-reference/webhooks/regenerate-secret) — Issue a new signing secret - [Delete webhook](/api-reference/webhooks/delete-webhook) — Permanently remove this webhook --- ## Get White-label Configuration > Retrieve the organization's white-label branding configuration URL: https://docs.storno.ro/api-reference/white-label-config/get # Get White-label Configuration Returns the organization's white-label branding configuration. White-label is available on the **Business** plan; the `entitled` field reflects whether the current plan allows it. Branding only takes effect when the plan is entitled **and** the configuration is enabled. ``` GET /api/v1/white-label-config ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token or API key for authentication | Requires the `settings.view` permission. ## Request ```bash {% title="cURL" %} curl https://api.storno.ro/api/v1/white-label-config \ -H "Authorization: Bearer YOUR_TOKEN" ``` ## Response ```json { "entitled": true, "data": { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "enabled": true, "appName": "Acme Invoicing", "logoUrl": "/v1/white-label/logo", "primaryColor": "#2563eb", "removeBranding": true } } ``` When no configuration has been created yet, `data` is `null`. ## Response Fields | Field | Type | Description | |-------|------|-------------| | `entitled` | boolean | Whether the organization's plan (Business) allows white-label | | `data` | object\|null | The configuration, or `null` if not yet created | | `data.enabled` | boolean | Whether white-label branding is active | | `data.appName` | string\|null | Custom app name shown in the app shell and browser tab | | `data.logoUrl` | string\|null | Relative URL of the custom logo (fetch with the same auth header) | | `data.primaryColor` | string\|null | Accent color in hex format | | `data.removeBranding` | boolean | Whether the Storno footer is removed from PDFs and client emails | ## Error Codes | Code | Description | |------|-------------| | `401` | Missing or invalid authentication token | | `403` | Missing `settings.view` permission | | `404` | Organization not found | --- ## Update White-label Configuration > Update the organization's white-label branding configuration URL: https://docs.storno.ro/api-reference/white-label-config/update # Update White-label Configuration Creates or updates the organization's white-label branding. Requires the **Business** plan; requests from other plans return `403`. Fields are merged — only the keys you send are changed. ``` PUT /api/v1/white-label-config ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token or API key for authentication | | `Content-Type` | string | Yes | `application/json` | Requires the `settings.manage` permission. ## Body Parameters | Field | Type | Required | Description | |-------|------|----------|-------------| | `enabled` | boolean | No | Enable or disable white-label branding | | `appName` | string\|null | No | Custom app name (max 100 chars, `null` to clear) | | `primaryColor` | string\|null | No | Accent color in hex format (e.g. `#2563eb`), `null` to clear | | `removeBranding` | boolean | No | Remove the `Storno.ro` footer from generated PDFs and client-facing emails | | `customDomain` | string\|null | No | Custom domain serving the app and client links (`null` to clear). Changing it resets verification and returns a `dnsRecord` to publish | The logo is managed through separate endpoints: `POST /api/v1/white-label-config/logo` (multipart upload, field `logo`, PNG/JPG/SVG up to 2MB) and `DELETE /api/v1/white-label-config/logo`. The custom domain is verified via [Verify Custom Domain](/api-reference/white-label-config/verify-domain). When a custom domain is set but not yet verified, the response includes a `dnsRecord` object (`{ name, type, value }`) — the TXT record to publish before verifying. ## Request ```bash {% title="cURL" %} curl -X PUT https://api.storno.ro/api/v1/white-label-config \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "enabled": true, "appName": "Acme Invoicing", "primaryColor": "#2563eb", "removeBranding": true }' ``` ## Response ```json { "data": { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "enabled": true, "appName": "Acme Invoicing", "logoUrl": "/v1/white-label/logo", "primaryColor": "#2563eb", "removeBranding": true } } ``` ## Error Codes | Code | Description | |------|-------------| | `400` | Invalid color format (must be hex, e.g. `#2563eb`) | | `401` | Missing or invalid authentication token | | `403` | Missing `settings.manage` permission, or plan is not Business | | `404` | Organization not found | --- ## Verify Custom Domain > Verify the white-label custom domain via its DNS TXT record URL: https://docs.storno.ro/api-reference/white-label-config/verify-domain # Verify Custom Domain Checks that the DNS TXT verification record for the configured custom domain has been published. On success the domain is marked verified and becomes the base for client-facing links (invoice share/payment URLs). Requires the **Business** plan. When you set a `customDomain` via [Update White-label Configuration](/api-reference/white-label-config/update), the response returns a `dnsRecord` to publish: ``` _storno-verify.facturi.example.com. TXT "a1b2c3d4e5f6..." ``` ``` POST /api/v1/white-label-config/domain/verify ``` ## Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | string | Yes | Bearer token or API key for authentication | Requires the `settings.manage` permission. ## Request ```bash {% title="cURL" %} curl -X POST https://api.storno.ro/api/v1/white-label-config/domain/verify \ -H "Authorization: Bearer YOUR_TOKEN" ``` ## Response ```json { "success": true, "data": { "customDomain": "facturi.example.com", "customDomainVerified": true, "customDomainVerifiedAt": "2026-06-02T10:30:00+00:00" } } ``` When the record is not found yet: ```json { "success": false, "error": "TXT record not found yet. DNS changes can take a few minutes to propagate.", "expected": { "name": "_storno-verify.facturi.example.com", "type": "TXT", "value": "a1b2c3d4e5f6..." } } ``` ## Next Step After verification, point the domain to Storno with a `CNAME` to `app.storno.ro`. TLS for the custom domain is provisioned by the Storno team once the CNAME resolves. ## Error Codes | Code | Description | |------|-------------| | `400` | No domain configured to verify | | `401` | Missing or invalid authentication token | | `403` | Missing `settings.manage` permission, or plan is not Business | | `404` | Organization not found | --- ## Contributing Guide > How to contribute to Storno — project setup, coding standards, and guidelines. URL: https://docs.storno.ro/contributing-guide # Contributing Guide Welcome to the Storno contributing guide. This section covers everything you need to get started contributing to the project, from environment setup to submitting pull requests. ## Project Overview Storno is a full-stack e-invoicing platform split across multiple repositories: - **[stornoro/storno](https://github.com/stornoro/storno)** — Monorepo containing backend (Symfony 7.4 API), frontend (Nuxt), and deploy configs - **[stornoro/storno-mobile-app](https://github.com/stornoro/storno-mobile-app)** — React Native / Expo mobile app - **[stornoro/storno-cli](https://github.com/stornoro/storno-cli)** — MCP-compatible CLI tool - **[stornoro/docs](https://github.com/stornoro/docs)** — API documentation (Next.js + Markdoc) - **[stornoro/status](https://github.com/stornoro/status)** — Uptime monitoring (Upptime) ## Getting Started Choose the platform you want to contribute to: - [Mobile App Setup](/contributing-guide/mobile-app/setup-guide) — Set up the React Native development environment - [Mobile App Architecture](/contributing-guide/mobile-app/architecture) — Understand the mobile app's architecture and patterns - [Mobile App Common Errors](/contributing-guide/mobile-app/common-errors) — Troubleshooting common development issues - [Translation Guidelines](/contributing-guide/mobile-app/translation-guidelines) — Adding or updating translations ## Branching & Commits The project uses `main` as the primary branch. When working on changes: - **Features:** `feature/` - **Fixes:** `fix/` - **Chores:** `chore/` Write concise commit messages that focus on _why_ the change was made, not just _what_ changed. ## Full-Stack Parity When making changes that affect plans, features, types, or i18n labels, **always update all platforms** across their respective repositories: 1. **Backend** — `stornoro/storno` → `backend/src/` 2. **Frontend** — `stornoro/storno` → `frontend/app/` + `frontend/i18n/` 3. **Mobile** — `stornoro/storno-mobile-app` → `src/` + `app/` 4. **Docs** — `stornoro/docs` → `content/` When adding new API endpoints to the backend, also add corresponding MCP tools to `stornoro/storno-cli` → `src/tools/`. ## Coding Standards - TypeScript strict mode is enabled across all platforms - Use ESLint for linting (`npm run lint`) - Follow existing patterns in the codebase — consistency is more important than personal preference - Keep PRs focused and small when possible - Include meaningful PR descriptions that explain the _why_ ## Quick Links - [API Documentation](/getting-started/quickstart) — API reference and conventions - [Authentication](/getting-started/authentication) — Auth flow details - [Self-Hosting](/getting-started/self-hosting) — Running the platform locally --- ## Common Errors > Troubleshooting common issues when developing the Storno mobile app. URL: https://docs.storno.ro/contributing-guide/mobile-app/common-errors # Common Errors This page covers common issues you may encounter while developing the Storno mobile app and how to resolve them. ## Setup & Installation ### `expo: command not found` Expo CLI is not installed globally. ```bash npm install -g expo-cli eas-cli ``` Or run Expo commands via npx: ```bash npx expo start ``` ### npm Install Fails with Peer Dependency Errors When adding new packages, always use `npx expo install` instead of `npm install`: ```bash # Correct npx expo install @react-native-async-storage/async-storage # Incorrect — may install incompatible versions npm install @react-native-async-storage/async-storage ``` Expo automatically resolves the correct version compatible with your SDK. ### Node.js Version Mismatch If you see errors about incompatible Node.js versions, make sure you're using the LTS version (v20+): ```bash node --version # Should be v20.x or higher ``` Use [nvm](https://github.com/nvm-sh/nvm) to manage Node versions: ```bash nvm install --lts nvm use --lts ``` ## Build Errors ### `expo prebuild` Fails If native code generation fails: 1. Delete the existing native directories and retry: ```bash rm -rf android ios npm run prebuild:android ``` 2. Clear the Expo cache: ```bash npx expo start --clear ``` 3. Make sure all native dependencies are properly listed in `app.config.ts` plugins. ### Android Build Fails — SDK Not Found Ensure `ANDROID_HOME` is set correctly: ```bash echo $ANDROID_HOME # Should output: /Users//Library/Android/sdk ``` If not set, add to your `~/.zshrc`: ```bash export ANDROID_HOME=$HOME/Library/Android/sdk export PATH=$PATH:$ANDROID_HOME/emulator export PATH=$PATH:$ANDROID_HOME/platform-tools ``` ### iOS Build Fails — CocoaPods Issues If iOS builds fail with CocoaPods errors: ```bash cd ios pod install --repo-update cd .. ``` If that doesn't work, clean and rebuild: ```bash rm -rf ios/Pods ios/Podfile.lock cd ios && pod install && cd .. ``` ### EAS Build Fails Check the EAS CLI version: ```bash eas --version eas update # Update if needed ``` Review the build logs on the [Expo dashboard](https://expo.dev) for detailed error messages. ## Runtime Errors ### Metro Bundler — Module Not Found If Metro can't resolve a module: 1. Clear the Metro cache: ```bash npx expo start --clear ``` 2. Delete `node_modules` and reinstall: ```bash rm -rf node_modules npm install ``` 3. If using a native module, make sure you've run `expo prebuild`. ### White Screen on Launch Usually caused by an unhandled error during app initialization. Check the Metro terminal output for JavaScript errors. Common causes: - Missing environment variables - API server not reachable - Corrupted secure storage data To reset secure storage on the simulator: - **iOS:** Reset the simulator (Device > Erase All Content and Settings) - **Android:** Clear app data (Settings > Apps > Storno > Clear Data) ### Network Request Failed (Axios) If API calls fail with network errors: 1. **Local development:** Make sure the backend server is running at the configured URL 2. **Android emulator:** Use `10.0.2.2` instead of `localhost` to reach the host machine 3. **iOS simulator:** `localhost` works, but verify the port matches your backend 4. **Physical device:** Use the machine's local IP address or a tunnel like `ngrok` You can change the server host in the app's settings screen without rebuilding. ### Token Refresh Loop If the app keeps logging you out or shows repeated 401 errors: 1. The refresh token may have expired — log out and log back in 2. Check if the backend server's clock is synchronized 3. Verify the backend is returning valid JWT tokens ### WebSocket Connection Failed If real-time updates aren't working: 1. Check that the Centrifugo server is running 2. Verify the WebSocket URL in the environment configuration 3. Check the app's network connectivity 4. Look for connection errors in the Metro console ## TypeScript Errors ### Type Errors After Updating Types If you've updated type definitions and see cascading errors: 1. Restart the TypeScript language server in your editor 2. Make sure all affected files use the updated types 3. Run `npx tsc --noEmit` to check for type errors project-wide ### Strict Mode Violations The project uses TypeScript strict mode. Common issues: - **Implicit `any`:** Always provide explicit types for function parameters - **Null checks:** Use optional chaining (`?.`) or null guards before accessing properties - **Missing return types:** Add explicit return types to exported functions ## Linting Errors ### ESLint Errors on Commit Run the linter before committing: ```bash npm run lint ``` The project uses the official Expo ESLint flat config. Most issues can be auto-fixed, but the project doesn't include a `--fix` script — review changes manually to ensure correctness. ## Fastlane / Store Deployment ### Google Play Metadata Upload Fails 1. Verify the service account key exists at `fastlane/google-play-key.json` 2. Check that the service account has the correct Google Play Console permissions 3. Ensure the app is already created in the Google Play Console ### Screenshot Dimensions Rejected Google Play requires specific screenshot dimensions. Screenshots should be placed in: ``` fastlane/metadata/android//images/phoneScreenshots/ ``` Supported locales: `en-US`, `ro` ## Getting Help If you encounter an issue not listed here: 1. Check the Metro bundler terminal for detailed error logs 2. Search the [Expo documentation](https://docs.expo.dev) 3. Check [React Native's troubleshooting guide](https://reactnative.dev/docs/troubleshooting) 4. Review the Sentry dashboard for crash reports (if configured) --- ## Mobile App Architecture > Overview of the Storno mobile app's architecture, patterns, and conventions. URL: https://docs.storno.ro/contributing-guide/mobile-app/architecture # Mobile App Architecture This guide explains the architecture and conventions used in the Storno mobile app. Understanding these patterns will help you contribute effectively. ## Tech Stack | Layer | Technology | Version | |-------|-----------|---------| | Framework | React Native | 0.81.x | | Platform | Expo | 54.x | | Navigation | Expo Router | 6.x | | Language | TypeScript | 5.9.x (strict mode) | | State Management | Zustand | 5.x | | Server State | TanStack React Query | 5.x | | HTTP Client | Axios | 1.x | | Real-time | Centrifuge (WebSocket) | 5.x | | i18n | i18next + react-i18next | 25.x / 16.x | | Auth Storage | Expo Secure Store | 15.x | | Error Tracking | Sentry | 7.x | ## Project Structure ``` mobile/ ├── app/ # Expo Router — file-based navigation │ ├── (auth)/ # Auth screens (login, register, MFA) │ ├── (tabs)/ # Main app with tab navigation │ │ ├── invoices/ # Invoice CRUD │ │ ├── proforma-invoices/ # Proforma invoices │ │ ├── delivery-notes/ # Delivery notes │ │ ├── receipts/ # Receipts │ │ ├── efactura/ # e-Factura status & messages │ │ ├── notifications/ # Notification list │ │ └── menu/ # Settings, clients, products, reports │ ├── _layout.tsx # Root layout │ └── +not-found.tsx # 404 screen ├── src/ │ ├── api/ # API modules (one file per feature) │ ├── components/ # React components by feature │ │ ├── ui/ # Shared UI primitives │ │ ├── shared/ # Shared business components │ │ ├── invoices/ # Invoice-specific components │ │ ├── clients/ # Client-specific components │ │ └── ... # Other feature components │ ├── hooks/ # Custom React hooks │ ├── stores/ # Zustand stores │ ├── theme/ # Design tokens (colors, spacing, typography) │ ├── types/ # TypeScript type definitions │ ├── i18n/ # Translation files (en.ts, ro.ts) │ ├── utils/ # Utilities (storage, dates, permissions, etc.) │ └── constants/ # App constants ├── assets/ # Icons, splash screen, images ├── fastlane/ # Google Play deployment automation ├── scripts/ # Helper scripts ├── app.config.ts # Expo configuration ├── eas.json # EAS build profiles └── package.json ``` ## Navigation The app uses **Expo Router** with file-based routing. Routes are defined by the file structure in `app/`. ### Route Groups - `(auth)` — Authentication screens: login, register, forgot-password, confirm-email, mfa-verify - `(tabs)` — Main application with bottom tab navigation ### Tab Bar The bottom tab bar shows 5 visible tabs: 1. **Dashboard** — Overview statistics 2. **Invoices** — Invoice management 3. **e-Factura** — ANAF e-invoice status 4. **Notifications** — In-app notifications 5. **Menu** — Settings, clients, products, reports, and more Additional document types (proforma invoices, delivery notes, receipts) are accessible via navigation but hidden from the tab bar. ### Navigation Conventions - Tab press at root level emits scroll-to-top / refresh events - Tab press on deep stacks pops back to the root screen - Use `router.push()` for forward navigation, `router.back()` for going back - Use `router.replace()` when you don't want the user to go back (e.g., after login) ## State Management The app uses a two-layer state approach: ### Global State — Zustand Zustand stores manage app-wide state that isn't tied to server data: | Store | Purpose | |-------|---------| | `authStore` | Authentication tokens, user data, login/logout flows | | `companyStore` | Company list, selected company, company switching | | `settingsStore` | User preferences (server host, biometrics, language) | | `toastStore` | Toast notification queue | | `confirmStore` | Confirmation dialog state | | `actionSheetStore` | Action sheet modal state | ### Server State — React Query TanStack React Query manages all data fetched from the API: - **Stale time:** 5 minutes — data is considered fresh for 5 minutes - **Retry:** 2 attempts on failure - **Refetch on focus:** Enabled — data refreshes when app comes to foreground - **Cache isolation:** Company-scoped queries are cleared when switching companies ```typescript // Example: Fetching invoices const { data, isLoading } = useQuery({ queryKey: ['invoices', companyId, filters], queryFn: () => invoiceApi.list(filters), }); ``` ## API Layer ### Client Configuration Two Axios instances exist in `src/api/client.ts`: - **`authClient`** — No interceptors, used for login/register - **`client`** — Auth interceptors attached, used for all authenticated requests ### Request Interceptors The authenticated client automatically: 1. Attaches `Authorization: Bearer ` header 2. Attaches `X-Organization` header 3. Attaches `X-Company` header (from selected company) 4. Sets `baseURL` from settings store ### Response Interceptors On receiving a `401 Unauthorized`: 1. Attempts to refresh the token using the refresh token 2. Retries the original request with the new token 3. Logs the user out if refresh fails ### API Module Convention Each feature has its own API file in `src/api/`: ```typescript // src/api/invoices.ts export const invoiceApi = { list: (params) => client.get('/v1/invoices', { params }), detail: (uuid) => client.get(`/v1/invoices/${uuid}`), create: (data) => client.post('/v1/invoices', data), update: (uuid, data) => client.put(`/v1/invoices/${uuid}`, data), delete: (uuid) => client.delete(`/v1/invoices/${uuid}`), }; ``` ## Real-time Updates The app uses **Centrifuge** (WebSocket) for real-time updates: - Invoice changes, company updates, and notifications arrive in real-time - The WebSocket connection is managed across app state transitions (foreground/background) - Channel-based subscriptions with automatic reconnection - Configured in `src/api/centrifugo.ts` ## Authentication Flow 1. **Login** — Email/password, Google Sign-In, or Passkey 2. **MFA** — If enabled, prompts for TOTP or backup code 3. **Token Storage** — JWT and refresh token stored in Expo Secure Store (encrypted) 4. **Biometric Lock** — Optional Face ID / Touch ID when app is backgrounded 5. **Auto-refresh** — Tokens are automatically refreshed on 401 responses 6. **Logout** — Clears tokens, user data, and query cache ## Permissions The app uses a granular permission system: ```typescript import { usePermissions, P } from '../hooks/usePermissions'; const { can } = usePermissions(); if (can(P.INVOICES_CREATE)) { // Show create button } ``` Permissions are checked throughout the UI to show/hide features based on the user's role. ## Component Architecture ### Feature Components Components are organized by feature domain in `src/components/`: ``` components/ ├── ui/ # Reusable UI primitives (Button, Input, Card, etc.) ├── shared/ # Shared business components (filters, pickers, etc.) ├── invoices/ # Invoice list items, forms, detail sections ├── clients/ # Client-related components └── ... ``` ### Styling - Pure React Native `StyleSheet` — no external CSS or Tailwind - Design tokens from `src/theme/` (colors, spacing, typography) - Light theme only (currently) - Consistent use of the color palette defined in `theme/colors.ts` ## Error Handling - **Sentry** captures uncaught errors and breadcrumbs - **ErrorBoundary** wraps the root layout - **Toast notifications** for user-facing errors - **`translateApiError()`** utility converts backend error codes to i18n keys ## Key Conventions When contributing, follow these patterns: 1. **One API file per feature** in `src/api/` 2. **Use React Query hooks** for data fetching — don't call API functions directly in components 3. **Use Zustand stores** only for app-wide state not tied to server data 4. **Use `useTranslation()`** for all user-facing text — never hardcode strings 5. **Follow the existing component structure** — feature components in their domain folder, shared components in `ui/` or `shared/` 6. **TypeScript strict mode** — all types must be explicit, no `any` --- ## Mobile App Setup Guide > Set up the Storno mobile app for local development on macOS. URL: https://docs.storno.ro/contributing-guide/mobile-app/setup-guide # Mobile App Setup Guide This guide walks you through setting up the Storno mobile app for local development. ## Prerequisites Before you begin, make sure you have the following installed: | Tool | Version | Purpose | |------|---------|---------| | [Node.js](https://nodejs.org/) | LTS (v20+) | JavaScript runtime | | [npm](https://www.npmjs.com/) | Bundled with Node.js | Package manager | | [Expo CLI](https://docs.expo.dev/get-started/set-up-your-environment/) | Latest | React Native development platform | | [EAS CLI](https://docs.expo.dev/build/setup/) | Latest | Build and submit to app stores | | [Xcode](https://developer.apple.com/xcode/) | Latest | iOS builds (macOS only) | | [Android Studio](https://developer.android.com/studio) | Latest | Android builds and emulators | | [Watchman](https://facebook.github.io/watchman/) | Latest | File watching (recommended on macOS) | ### Install Expo CLI and EAS CLI ```bash npm install -g expo-cli eas-cli ``` ### iOS Setup (macOS) 1. Install Xcode from the Mac App Store 2. Open Xcode and install the iOS Simulator runtime 3. Accept the Xcode license: `sudo xcodebuild -license accept` 4. Install CocoaPods: `sudo gem install cocoapods` ### Android Setup 1. Install Android Studio 2. In Android Studio, install the Android SDK (API 34+) 3. Configure `ANDROID_HOME` in your shell profile: ```bash export ANDROID_HOME=$HOME/Library/Android/sdk export PATH=$PATH:$ANDROID_HOME/emulator export PATH=$PATH:$ANDROID_HOME/platform-tools ``` 4. Create an Android Virtual Device (AVD) in Android Studio with API 28+ (minimum SDK) ## Clone & Install ```bash # Navigate to the mobile directory cd mobile # Install dependencies npm install ``` > **Note:** Always use `npx expo install ` when adding new dependencies instead of `npm install `. Expo will automatically install the correct compatible version for your SDK. ## Environment Configuration The mobile app uses Expo environment variables defined in `eas.json` for different build profiles. For local development, the app connects to the API based on the build profile: | Profile | API URL | Description | |---------|---------|-------------| | `development` | `https://api.storno.test:8000` | Local development server | | `preview` | `https://staging.storno.ro` | Staging environment | | `production` | `https://api.storno.ro` | Production | You can change the server host at runtime from the app's settings screen (useful for switching between environments during development). ### Sentry (Optional) If you need error tracking in development, create a `.env` file: ```bash SENTRY_AUTH_TOKEN=your_sentry_auth_token ``` This is optional for local development. ## Running the App ### Start the Development Server ```bash npm start ``` This launches the Expo development server. You'll see a QR code and options to open the app. ### Run on iOS Simulator ```bash npm run ios ``` This will build the native iOS project and launch it in the iOS Simulator. ### Run on Android Emulator ```bash npm run android ``` Make sure an Android emulator is running or a device is connected via USB before running this command. ### Run on Web ```bash npm run web ``` > **Note:** The web version has limited functionality compared to native platforms. ## Prebuild (Native Code Generation) When you add a new native dependency or modify `app.config.ts`, you need to regenerate the native directories: ```bash # Android only npm run prebuild:android ``` This runs `expo prebuild --platform android --clean` and regenerates the `android/` directory. For iOS, the native code is generated during `npm run ios`. ## Building for Distribution ### EAS Cloud Builds ```bash # iOS npm run build:ios:preview # Internal testing npm run build:ios:production # App Store # Android npm run build:android:preview # Internal testing npm run build:android:production # Google Play ``` ### Local Android Build ```bash npm run prebuild:android npm run build:android:local ``` This produces an `.aab` file in `android/app/build/outputs/bundle/release/`. ### Submit to Stores ```bash # iOS — requires App Store Connect credentials npm run submit:ios # Android — requires Google Play service account key npm run submit:android # Build + submit in one step npm run build:submit:ios npm run build:submit:android ``` ## Google Play Metadata Manage Google Play Store metadata and screenshots via Fastlane: ```bash npm run gplay:metadata # Upload title, descriptions npm run gplay:screenshots # Upload screenshots npm run gplay:all # Upload everything ``` Metadata files are in `fastlane/metadata/android/` organized by locale (`en-US/`, `ro/`). ## Linting ```bash npm run lint ``` Uses ESLint with the official Expo flat config. Fix any lint errors before submitting a PR. ## Next Steps - Read the [Architecture Guide](/contributing-guide/mobile-app/architecture) to understand the codebase - Check [Common Errors](/contributing-guide/mobile-app/common-errors) if you run into issues - Review the [Translation Guidelines](/contributing-guide/mobile-app/translation-guidelines) if working on i18n --- ## Translation Guidelines > How to add or update translations in the Storno mobile app. URL: https://docs.storno.ro/contributing-guide/mobile-app/translation-guidelines # Translation Guidelines The Storno mobile app supports multiple languages. This guide covers how translations work and how to add or update them. ## Supported Languages | Language | Code | Status | |----------|------|--------| | Romanian | `ro` | Primary language | | English | `en` | Secondary language | Romanian is the primary language since Storno is focused on the Romanian market (e-Factura / ANAF integration). English is the fallback language. ## How i18n Works The app uses **i18next** with **react-i18next** for internationalization: - Translation files are TypeScript objects in `src/i18n/` - The language is auto-detected from the device locale on first launch - Users can manually change the language in the app settings - The selected language is persisted to local storage ### File Structure ``` src/i18n/ ├── index.ts # i18next configuration ├── en.ts # English translations └── ro.ts # Romanian translations ``` ### Using Translations in Components ```typescript import { useTranslation } from 'react-i18next'; function MyComponent() { const { t } = useTranslation(); return {t('invoices.title')}; } ``` ## Adding New Translation Keys When adding a new feature that includes user-facing text: ### 1. Add to Both Language Files Always add the key to **both** `en.ts` and `ro.ts` simultaneously: ```typescript // src/i18n/en.ts export const en = { // ... myFeature: { title: 'My Feature', description: 'This is a new feature', actions: { save: 'Save', cancel: 'Cancel', }, }, }; // src/i18n/ro.ts export const ro = { // ... myFeature: { title: 'Funcția mea', description: 'Aceasta este o funcție nouă', actions: { save: 'Salvează', cancel: 'Anulează', }, }, }; ``` ### 2. Use Consistent Key Naming Follow the existing naming conventions: - **Namespace by feature:** `invoices.title`, `clients.actions.delete` - **Use camelCase** for key names - **Group related keys** under a common parent (e.g., `actions`, `errors`, `labels`) - **Use descriptive names** — `invoices.emptyState` not `invoices.es` ### 3. Never Hardcode Strings All user-facing text must go through the translation system: ```typescript // Correct {t('invoices.noResults')} // Incorrect — hardcoded string No invoices found ``` ## Updating Existing Translations When modifying an existing translation: 1. Update the key in **both** language files 2. Make sure the meaning is preserved in both languages 3. If you're unsure about a Romanian translation, leave a comment in the PR ## Translation Key Structure The translation files follow a hierarchical structure organized by feature: ``` root ├── common # Shared terms (save, cancel, delete, loading, etc.) ├── auth # Login, register, MFA screens ├── invoices # Invoice list, detail, create/edit ├── proforma # Proforma invoices ├── deliveryNotes # Delivery notes ├── receipts # Receipts ├── clients # Client management ├── suppliers # Supplier management ├── products # Product management ├── companies # Company management ├── efactura # e-Factura / ANAF integration ├── dashboard # Dashboard statistics ├── settings # Settings screens ├── notifications # Notifications ├── reports # Reports (VAT, sales, balances) ├── errors # Error messages └── validation # Form validation messages ``` ## Best Practices 1. **Keep translations short** — Mobile screens have limited space. Prefer concise text. 2. **Be consistent** — Use the same term for the same concept everywhere (e.g., always "Factură" for invoice, not sometimes "Factură" and sometimes "Document fiscal"). 3. **Preserve placeholders** — If a translation contains dynamic values like `{{count}}` or `{{name}}`, keep them in both languages. 4. **Don't translate brand names** — "Storno", "e-Factura", "ANAF" stay as-is in all languages. 5. **Test on device** — After adding translations, verify that the text fits on screen without being truncated. ## Full-Stack Parity When updating translations, remember that the same changes may need to be applied across: - **Frontend** — `frontend/i18n/` - **Mobile** — `mobile/src/i18n/` Keep terminology consistent across all platforms. --- ## Changelog > API version history and breaking changes. URL: https://docs.storno.ro/changelog # Changelog All notable changes to the Storno.ro API are documented here. ## 2026-06-02 ### Changed - **Stripe App refunds** — refunding a Stripe payment now issues a storno reversal of the original e-invoice (negated quantities, inheriting the original series, document type, and per-line VAT rates) instead of a synthetic single-line credit note. A full refund reverses the whole invoice; a partial refund reverses proportionally. Surfaced in the app's **Payment Detail → Refunds** section. ## 2026-04-26 — v2.7.0 ### Added - **Refund receipts** — `POST /receipts/{uuid}/refund` issues a counter-receipt that mirrors lines as negative and inverts payment amounts. Supports full or partial refunds via the `lineSelections` body field; multiple partial refunds against the same parent are allowed until the per-line quantity pool is exhausted. Cancelling a refund releases its quantities back to the pool. - **Receipt linkage fields** — `Receipt.refundOf` (slim `{id, number}` reference to the parent receipt) and `Receipt.refundedBy` (array of slim refs to active refund receipts; cancelled refunds are excluded). - **Idempotency keys for receipts** — `Receipt.idempotencyKey` (unique varchar 255) accepted via the `Idempotency-Key` HTTP header (preferred) or the `idempotencyKey` body field. Repeat submissions with the same key return the originally-created receipt instead of duplicating. Used by mobile POS for safe offline retries and ambiguous-timeout recovery. - **Product categories** — new `ProductCategory` entity with `name`, `color`, `sortOrder`. Full CRUD under `/product-categories`. Optional FK on `Product.category` with `ON DELETE SET NULL`. Used as fallback swatch and grid grouping on the POS. - **Product fields** — `Product.color` (optional hex swatch shown on the POS product grid; mobile clients fall back to a deterministic palette derived from the product UUID when null) and `Product.sgrAmount` (Romanian SGR / Sistem Garantie-Returnare deposit per unit, e.g. `"0.50"` for plastic beverage bottles; the deposit is VAT-exempt and appears as a separate auto-managed line on POS receipts). - **Cash register endpoints** — `GET /cash-register/balance`, `GET /cash-register/ledger`, and full CRUD on `/cash-register/movements` (deposits, withdrawals, miscellaneous adjustments). Bank accounts gain `type=cash` with `openingBalance` + `openingBalanceDate` to back the till. ### Changed - `Idempotency-Key` HTTP header now takes precedence over the body `idempotencyKey` field when both are sent (fixes inverted precedence in earlier preview). - Refund receipts inherit `internalNote`, `cashRegisterName`, `fiscalNumber`, and customer fiscal data from the parent receipt. - Receipt detail PDFs render `BON DE RAMBURSARE` instead of `BON FISCAL` for refund receipts (ro/en/fr/de translations included). ## 2026-02-16 ### Documentation - Published comprehensive API documentation covering all endpoints - Added object reference for all entity types - Added concept guides for multi-tenancy, ANAF integration, document lifecycle, series numbering, and recurring invoices ## v1 (Current) ### Features - **Authentication** — JWT tokens, refresh tokens, Google OAuth, WebAuthn passkeys - **User Management** — Registration, password reset, email confirmation, profile management, account deletion - **Organizations** — Multi-tenant with role-based memberships (Owner, Admin, Accountant, Employee) - **Companies** — Multi-company support with ANAF CIF validation - **Invoices** — Full lifecycle: create, issue, submit to ANAF, cancel, restore - **Proforma Invoices** — Create, send, accept/reject, convert to invoice - **Delivery Notes** — Create, issue, cancel, convert to invoice - **Credit Notes** — Create and submit as corrective invoices - **Recurring Invoices** — Scheduled automatic invoice generation with flexible frequencies - **Payments** — Record, track, and manage payments per invoice - **ANAF Integration** — OAuth token management, e-Factura sync, XML validation, digital signature verification - **PDF Generation** — Professional PDF invoices from UBL XML - **Email** — Send invoices with PDF/XML attachments, customizable templates - **Export** — CSV and ZIP export of invoices - **Reports** — VAT reports by period - **Notifications** — In-app, email, and push notifications with preferences - **Real-time** — WebSocket updates via Centrifugo - **Exchange Rates** — BNR exchange rates with currency conversion - **Admin** — Super admin platform management endpoints ### API Conventions - All endpoints under `/api/v1/` - JWT Bearer authentication - Multi-company context via `X-Company` header - JSON request/response bodies - UUID resource identifiers - Paginated list responses with `page`, `limit`, `total`, `pages` --- ## FAQ > Frequently asked questions about the Storno.ro API and platform. URL: https://docs.storno.ro/faq # FAQ ## General ### What is Storno.ro? Storno.ro is an e-invoicing platform that integrates with e-invoice provider systems across the EU (Romania, Germany, Italy, Poland, France). It provides a REST API, web application, and mobile app for creating, managing, and submitting invoices electronically. ### What document types are supported? - **Invoices** — Outgoing and incoming, with multi-country e-invoicing integration - **Proforma invoices** — Quotations and advance billing - **Credit notes** — Invoice corrections and refunds - **Delivery notes** — Goods shipment documentation - **Recurring invoices** — Scheduled automatic generation ### What is e-Factura? e-Factura is Romania's mandatory electronic invoicing system operated by ANAF (the national tax authority). All B2B invoices in Romania must be submitted through the e-Factura SPV (Virtual Private Space). Storno.ro automates this process — generating UBL 2.1 XML, submitting to ANAF, and tracking validation status. --- ## API ### How do I authenticate? Use email/password, Google OAuth, or passkeys to obtain a JWT token. Include it in the `Authorization: Bearer {token}` header. Most endpoints also require an `X-Company` header. See [Authentication](/getting-started/authentication). ### What format are API responses? All responses use JSON. Dates use ISO 8601 (`YYYY-MM-DD`), timestamps include timezone (`YYYY-MM-DDTHH:mm:ssZ`), and all identifiers are UUIDs. ### What are the rate limits? Authenticated requests allow up to 50,000 requests with a refill rate of 500 per 2 minutes. Authentication endpoints have stricter limits (e.g., 5 login attempts per minute). See [Rate Limiting](/getting-started/rate-limiting). ### Can I use API keys instead of JWT tokens? Yes. Create scoped API keys via [API Keys](/api-reference/api-keys/create) for programmatic access. API keys can optionally have an expiration date and can be revoked instantly. Include the key directly in the `Authorization` header (without the `Bearer` prefix): `Authorization: af_...` ### How does pagination work? List endpoints accept `page` and `limit` query parameters. Responses include `total`, `page`, `limit`, and `pages` metadata. See [Pagination](/getting-started/pagination). --- ## Self-Hosting ### Can I self-host Storno.ro? Yes. Self-hosting requires a license key. Deploy with Docker Compose in minutes. See [Self-Hosting](/getting-started/self-hosting). ### What data is sent to Storno.ro from my self-hosted instance? Nothing. License keys are signed JWTs validated offline — no network calls are made. No user data, invoices, or business information ever leaves your server. ### Does the license require internet connectivity? No. License keys are signed JWTs validated entirely offline on your server. No network calls are made to the SaaS server. Your instance works fully offline, air-gapped, or behind a firewall. ### What are the minimum server requirements? A small deployment (< 5 users) needs 2 vCPUs, 4 GB RAM, and 20 GB SSD. See [System Requirements](/getting-started/system-requirements) for detailed specifications. ### How do I upgrade my self-hosted instance? ```bash docker compose pull docker compose up -d docker compose exec backend php bin/console doctrine:migrations:migrate --no-interaction ``` ### How do I back up my data? Database backups via `mysqldump` and volume backups for file storage. Per-company backup/restore is also available via the API. See the Backups section in [Self-Hosting](/getting-started/self-hosting). --- ## e-Factura ### How often does Storno.ro sync with ANAF? Sync frequency depends on your plan. Freemium syncs once per day, Starter every 12 hours, Professional every 4 hours, and Business every hour. You can also trigger a manual sync via the API. ### What happens when ANAF rejects an invoice? The invoice status changes to `rejected` and the ANAF error messages are stored. You can view the errors, fix the invoice, and resubmit. ### Can Storno.ro receive incoming invoices from ANAF? Yes. The sync process automatically downloads incoming invoices from ANAF SPV, parses the UBL XML, and creates Invoice records with the associated client and line items. ### Does Storno.ro validate XML before submission? Yes. Invoices are validated against the UBL 2.1 schema and ANAF Schematron rules before submission. The [Validate Invoice](/api-reference/invoices/validate) endpoint lets you check for errors without submitting. --- ## Security ### What authentication methods are supported? Email/password, Google OAuth, WebAuthn passkeys, and scoped API keys. Two-factor authentication (TOTP) is available for email/password and Google OAuth logins. See [Security](/getting-started/security). ### How do I report a security vulnerability? Email [security@storno.ro](mailto:security@storno.ro). Do not open a public issue. We acknowledge reports within 48 hours. See [Security](/getting-started/security). ### Are passkey logins affected by MFA? No. Passkeys inherently satisfy multi-factor authentication (device possession + biometric/PIN), so no additional MFA challenge is required. --- ## Webhooks ### What events can I subscribe to? Invoice lifecycle events (created, issued, submitted, validated, rejected, paid), payment events, client events, and more. See [List Webhook Event Types](/api-reference/webhooks/list-events) for the complete list. ### How are webhooks secured? Each webhook endpoint has an HMAC-SHA256 signing secret. Verify the `X-Webhook-Signature` header on incoming requests to confirm authenticity. See [Webhooks & Events](/concepts/webhooks-events). ### What happens if my webhook endpoint is down? Storno.ro retries failed deliveries up to 3 times with exponential backoff. You can inspect delivery history and payloads via [List Webhook Deliveries](/api-reference/webhooks/list-deliveries).