# Fina Developers — full documentation > Generated from the same source as https://fina.do/developers. Index: https://fina.do/llms.txt. OpenAPI specs: https://fina.do/api/wallet/v1/openapi.json, https://fina.do/api/farm/v1/openapi.json, https://fina.do/api/boutique/v1/openapi.json, https://fina.do/api/restaurant/v1/openapi.json, https://fina.do/api/barbershop/v1/openapi.json, https://fina.do/api/real-estate/v1/openapi.json, https://fina.do/api/rent-car/v1/openapi.json, https://fina.do/api/staff/v1/openapi.json, https://fina.do/api/video-studio/v1/openapi.json. ## Overview > What you can build on Fina, which API to use and how to authenticate. Fina is the operating system for small businesses in the Dominican Republic: books, a wallet (**Fina Pay**) and vertical modules (farm, store, restaurant, barbershop, real estate, car rental). Everything a third party can use is a plain HTTPS + JSON API described by an **OpenAPI 3.1** spec. | I want to… | Use | Credential | | --- | --- | --- | | Accept payments in my store, app or backend | [Fina Pay API](https://fina.do/developers/api-fina-pay.md) | Secret key `sk_live_…` | | Build my own customer app or website for a Fina business | [Storefront APIs](https://fina.do/developers/storefront-apis.md) | App key of the module | | Build a kitchen display / POS for my restaurant | [Restaurant staff API](https://fina.do/developers/api-staff.md) | Paired device + staff PIN | | Automate AI media jobs with my credits | [Video Studio API](https://fina.do/developers/api-video-studio.md) | Personal key `fina_vs_…` | #### Conventions shared by every API - Money is always an **integer in minor units** (`amount_minor`, `price_minor`…): `150000` = RD$1,500.00. Never a float. - Timestamps are ISO 8601 in UTC (`2026-10-05T14:00:00.000Z`). Business dates (`YYYY-MM-DD`) are in Dominican Republic time (UTC−4). - Requests and responses are `application/json`. Error codes are stable strings — branch on them, never on the message. - Base host: `https://fina.do`. Every API is versioned in its path (`/v1`); breaking changes ship as a new version. #### Built for AI assistants Every page of these docs is also Markdown (append `.md` to its URL). [llms.txt](https://fina.do/llms.txt) indexes them, [llms-full.txt](https://fina.do/llms-full.txt) has all of it in one file, and a read-only [MCP server](https://fina.do/developers/ai.md) lets Claude Code, Cursor and other assistants search the docs and read the OpenAPI specs. Source: https://fina.do/developers#overview --- ## Quickstart > Create an API key, take your first payment and receive the webhook in about five minutes. #### 1. Create your API keys 1. Activate **Fina Pay** on your personal account in [/app/wallet](https://fina.do/app/wallet). 2. Open your business panel, edit your business and go to the **Connectors** tab → **Fina Pay**. 3. Create the connection: pick the income account and category where paid charges are booked. 4. Copy the **public key** (`pk_live_…`) and the **secret key** (`sk_live_…`). The secret is shown only once — store it as `FINA_SECRET_KEY` in your server environment. 5. Optional but recommended: set a **webhook URL** (HTTPS) and an **IPN secret**. Store the secret as `FINA_IPN_SECRET`. > **Warning:** The secret key moves money. Keep it on your server only — never in a browser, a mobile app or a Git repository. If it leaks, regenerate it from the same connector. #### 2. Create your first charge **POST /charges — curl** ```bash curl -X POST https://fina.do/api/wallet/v1/charges \ -H "Authorization: Bearer $FINA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount_minor": 150000, "description": "Order #42", "reference": "order-42", "metadata": { "order_id": "42" } }' ``` **POST /charges — TypeScript** ```ts // Node 18+, Bun, Deno or any edge runtime: no SDK needed. const res = await fetch('https://fina.do/api/wallet/v1/charges', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FINA_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount_minor: 150000, // RD$1,500.00 in centavos description: 'Order #42', reference: 'order-42', // idempotent: retrying returns the same charge }), }) if (!res.ok) throw new Error(`Fina Pay ${res.status}: ${(await res.json()).error}`) const charge: { id: string; status: string; payment_url: string } = await res.json() // Send the customer to Fina to pay: // res.redirect(charge.payment_url) ``` **POST /charges — Python** ```python import os import requests res = requests.post( "https://fina.do/api/wallet/v1/charges", headers={"Authorization": f"Bearer {os.environ['FINA_SECRET_KEY']}"}, json={ "amount_minor": 150000, # RD$1,500.00 in centavos "description": "Order #42", "reference": "order-42", # idempotent: retrying returns the same charge }, timeout=15, ) res.raise_for_status() charge = res.json() print(charge["payment_url"]) # redirect the customer here ``` **POST /charges — PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('FINA_SECRET_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'amount_minor' => 150000, // RD$1,500.00 in centavos 'description' => 'Order #42', 'reference' => 'order-42', // idempotent: retrying returns the same charge ]), ]); $charge = json_decode(curl_exec($ch), true); if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) !== 201) { throw new RuntimeException('Fina Pay: ' . ($charge['error'] ?? 'unknown_error')); } header('Location: ' . $charge['payment_url']); ``` **201 Created — JSON** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "status": "pending", "payment_url": "https://fina.do/pay/c/3f9c…", "amount_minor": 150000, "currency_code": "DOP", "reference": "order-42", "expires_at": "2026-10-06T14:00:00.000Z" } ``` Redirect the customer to `payment_url`. They sign in to Fina (or open the Fina app through the universal link), confirm with their wallet PIN and pay from their balance. #### 3. Receive the webhook When the charge is paid, Fina sends `POST` to your webhook URL with the event in the body and an `x-fina-sig` header. Verify the signature, then fulfil the order. **Webhook handler — TypeScript** ```ts // Express. Any framework works: you need the raw body and the x-fina-sig header. import express from 'express' import { verifyFinaSignature } from './fina' const app = express() app.post('/webhooks/fina', express.text({ type: 'application/json' }), async (req, res) => { if (!verifyFinaSignature(req.body, req.get('x-fina-sig') ?? '', process.env.FINA_IPN_SECRET!)) { return res.status(401).end() } const event = JSON.parse(req.body) if (event.event === 'charge.paid') { // Idempotent: a retried delivery must not ship the order twice. await markOrderPaid({ reference: event.reference, chargeId: event.charge_id, netMinor: event.net_minor }) } res.status(200).end() // answer 2xx fast; do slow work in a queue }) ``` **Webhook handler — Python** ```python # Flask import os from flask import Flask, request from fina import verify_fina_signature app = Flask(__name__) @app.post("/webhooks/fina") def fina_webhook(): raw = request.get_data() if not verify_fina_signature(raw, request.headers.get("x-fina-sig", ""), os.environ["FINA_IPN_SECRET"]): return "", 401 event = request.get_json() if event["event"] == "charge.paid": # Idempotent: a retried delivery must not ship the order twice. mark_order_paid(reference=event["reference"], charge_id=event["charge_id"], net_minor=event["net_minor"]) return "", 200 ``` **Webhook handler — PHP** ```php **Tip:** That is the whole integration. Next: read [Charges](https://fina.do/developers/charges.md) for statuses, fees and refunds, and [Testing](https://fina.do/developers/testing.md) before going live. Source: https://fina.do/developers#quickstart --- ## Build with AI > llms.txt, Markdown pages, a ready-made prompt and a read-only MCP server for Claude Code, Cursor and other assistants. These docs are designed to be read by your AI coding assistant as well as by you. Pick whichever fits your tool. #### Paste this prompt Works in Claude Code, Cursor, Windsurf, GitHub Copilot, ChatGPT or Claude.ai. Replace the last line with your task. **Prompt — Prompt** ```text I'm integrating Fina (https://fina.do) into my project. Before writing any code, read the full developer docs: https://fina.do/llms-full.txt Use the OpenAPI spec as the source of truth for request and response shapes: https://fina.do/api/wallet/v1/openapi.json (Fina Pay). Storefront and other APIs list their own spec in https://fina.do/llms.txt. Rules: - Keep the secret key (sk_live_…) on the server, read it from the FINA_SECRET_KEY env var, never commit it. - Amounts are integers in minor units (centavos). Never use floats for money. - Use my order id as `reference` so retries never create duplicate charges. - Verify the x-fina-sig webhook signature exactly as the docs describe, and make the webhook handler idempotent. Task: ``` #### Connect the docs MCP server A read-only [Model Context Protocol](https://modelcontextprotocol.io) server at `https://fina.do/api/developers/mcp` (Streamable HTTP, no auth). Tools: `search_docs`, `get_doc_page`, `list_apis`, `list_endpoints`, `get_endpoint` and `get_openapi_spec`. It only reads documentation — it never touches your account or your money. **Install — Claude Code** ```bash claude mcp add --transport http fina-docs https://fina.do/api/developers/mcp ``` **Install — Cursor** ```json // .cursor/mcp.json (project) or ~/.cursor/mcp.json (global) { "mcpServers": { "fina-docs": { "url": "https://fina.do/api/developers/mcp" } } } ``` **Install — VS Code** ```bash code --add-mcp '{"name":"fina-docs","type":"http","url":"https://fina.do/api/developers/mcp"}' ``` **Install — Other clients** ```json { "mcpServers": { "fina-docs": { "type": "http", "url": "https://fina.do/api/developers/mcp" } } } ``` #### LLM-friendly files | URL | What it is | | --- | --- | | [/llms.txt](https://fina.do/llms.txt) | Index in the [llmstxt.org](https://llmstxt.org) format: one link per page, to its Markdown version. | | [/llms-full.txt](https://fina.do/llms-full.txt) | All the documentation in one Markdown file, endpoint tables included. | | `/developers/.md` | Any single page as Markdown, e.g. [/developers/webhooks.md](https://fina.do/developers/webhooks.md). | | `…/openapi.json` | The machine-readable contract of each API (see [API reference](https://fina.do/developers/api-fina-pay.md)). | Every page also has **Copy as Markdown**, **Open in Claude** and **Open in ChatGPT** buttons. #### Example: an agent that creates Fina Pay charges If you build your own assistant with the Claude API, give it a tool that calls Fina Pay. Official Anthropic SDK, tool runner, model `claude-opus-5-5`. The tool only creates charges; the customer still confirms every payment in Fina with their PIN. **Claude + Fina Pay — TypeScript** ```ts // npm i @anthropic-ai/sdk zod import Anthropic from '@anthropic-ai/sdk' import { betaZodTool } from '@anthropic-ai/sdk/helpers/beta/zod' import { z } from 'zod' const client = new Anthropic() // reads ANTHROPIC_API_KEY const createFinaCharge = betaZodTool({ name: 'create_fina_charge', description: 'Create a Fina Pay charge and return the payment URL the customer must open to pay.', inputSchema: z.object({ amount_minor: z.number().int().positive().describe('Amount in centavos (RD$1.00 = 100)'), description: z.string(), reference: z.string().describe('Unique order id; makes retries idempotent'), }), run: async (input) => { const res = await fetch('https://fina.do/api/wallet/v1/charges', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FINA_SECRET_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(input), }) return JSON.stringify(await res.json()) }, }) const finalMessage = await client.beta.messages.toolRunner({ model: 'claude-opus-5-5', max_tokens: 16000, tools: [createFinaCharge], messages: [{ role: 'user', content: 'Charge RD$1,500 for order #42 (2 bags of coffee) and give me the link.' }], }) console.log(finalMessage.content) ``` **Claude + Fina Pay — Python** ```python # pip install anthropic requests import os import anthropic import requests from anthropic import beta_tool client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY @beta_tool def create_fina_charge(amount_minor: int, description: str, reference: str) -> str: """Create a Fina Pay charge and return the payment URL the customer must open to pay. Args: amount_minor: Amount in centavos (RD$1.00 = 100). description: What the customer is paying for. reference: Unique order id; makes retries idempotent. """ res = requests.post( "https://fina.do/api/wallet/v1/charges", headers={"Authorization": f"Bearer {os.environ['FINA_SECRET_KEY']}"}, json={"amount_minor": amount_minor, "description": description, "reference": reference}, timeout=15, ) return res.text runner = client.beta.messages.tool_runner( model="claude-opus-5-5", max_tokens=16000, tools=[create_fina_charge], messages=[{"role": "user", "content": "Charge RD$1,500 for order #42 (2 bags of coffee) and give me the link."}], ) for message in runner: print(message.content) ``` Source: https://fina.do/developers#ai --- ## Authentication > Public and secret keys of the Fina Pay connector, rate limits and key rotation. Each Fina Pay **connection** (one per integration, under your business → Connectors → Fina Pay) has a key pair: | Key | Format | Where | Used for | | --- | --- | --- | --- | | Secret key | `sk_live_…` | Server only | `Authorization: Bearer sk_live_…` on every charge, payout and recipient endpoint | | Public key | `pk_live_…` | Safe in a browser | Public lookup `GET /charges?token=…&public_key=…` for your own payment page | | IPN secret | You choose it | Server only | Verifying the `x-fina-sig` header of webhooks | **Header — HTTP** ```http Authorization: Bearer sk_live_… ``` - Fina stores only a hash of the secret key: it is shown once. Regenerating it invalidates the old one immediately. - Pausing or deleting the connection makes its keys answer `401 unauthorized`. - Rate limit: **60 requests per minute per connection**. Above that you get `429 rate_limited`; wait and retry with backoff. - Each connection books paid charges into the income account and category you chose, so use one connection per sales channel if you want separate reporting. Source: https://fina.do/developers#authentication --- ## Charges > Lifecycle of a Fina Pay charge: statuses, expiry, fees, cancellation and refunds. #### Lifecycle 1. Your backend calls `POST /charges`. The charge is `pending` and has a `payment_url`. 2. The customer opens `payment_url`, signs in to Fina and confirms with their wallet PIN. 3. The charge becomes `paid`; you receive `charge.paid` and `GET /charges/{id}` shows `paid_at`, `fee_minor` and `net_minor`. 4. Unpaid charges become `expired` at `expires_at` (24 h by default, `expires_in_seconds` to change it), or `cancelled` if you call `POST /charges/{id}/cancel`. | Status | Meaning | Webhook | | --- | --- | --- | | `pending` | Waiting for the customer | — | | `paid` | Money is in your Fina Pay wallet | `charge.paid` | | `expired` | Not paid before `expires_at` | `charge.expired` | | `cancelled` | You cancelled it while pending | `charge.cancelled` | | `refunded` | A paid charge was returned to the customer (fully or partly) | `charge.refunded` | #### Fees The customer always pays exactly `amount_minor`. Fina keeps the collection fee of your plan from what you receive: once paid, `fee_minor` is the fee and `net_minor = amount_minor − fee_minor` is what lands in your wallet. Before payment both are `null`. #### Idempotency `reference` is unique per connection. Calling `POST /charges` again with the same `reference` returns the existing charge (same `id`, same `payment_url`) instead of charging twice — so you can safely retry on timeouts. Use your order id. #### Refunds Refunds are issued from Fina (for example when a business rejects an order, cancels a table deposit or a subscription) and return the money — including the proportional fee — to the customer's wallet. There is no public refund endpoint yet; you are notified with `charge.refunded`. #### Get and cancel **GET /charges/{id} — curl** ```bash curl https://fina.do/api/wallet/v1/charges/CHARGE_ID \ -H "Authorization: Bearer $FINA_SECRET_KEY" ``` **GET /charges/{id} — Response** ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "status": "paid", "amount_minor": 150000, "fee_minor": 3750, "net_minor": 146250, "currency_code": "DOP", "reference": "order-42", "transfer_id": "8d2b…", "payment_url": "https://fina.do/pay/c/3f9c…", "expires_at": "2026-10-06T14:00:00.000Z", "paid_at": "2026-10-05T14:03:11.000Z" } ``` **POST /charges/{id}/cancel — curl** ```bash curl -X POST https://fina.do/api/wallet/v1/charges/CHARGE_ID/cancel \ -H "Authorization: Bearer $FINA_SECRET_KEY" ``` Full request and response schemas: [Fina Pay API reference](https://fina.do/developers/api-fina-pay.md). Source: https://fina.do/developers#charges --- ## Payouts > Pay Fina Pay users from your own wallet: withdrawals, prizes, refunds outside a charge. Idempotent and instant. A payout moves money from the Fina Pay wallet of the **connection owner** to another Fina user, in one atomic step. Fina does not create money: if your wallet does not have the balance, the payout is rejected. Use it to pay your users' withdrawals, prizes or anything you owe them. > **Warning:** Payouts are **off by default**. The business owner turns on **Payouts via API** in the Fina Pay connector (business → Connectors → Fina Pay). While it is on, anyone with the secret key can send money from that wallet: keep the key on your server only. #### Flow 1. Your user enters their Fina Pay account number (10 digits) or the phone linked to their wallet. 2. Call `GET /recipients?account=…` and show them the `name` the money will go to, so they can confirm it is them. 3. Call `POST /payouts` with your withdrawal id as `reference`. A `201` means the money is already in their wallet; they get a notification. 4. Store the payout `id`. `GET /payouts/{id}` returns it again at any time. **GET /recipients — curl** ```bash curl "https://fina.do/api/wallet/v1/recipients?account=8095551234" \ -H "Authorization: Bearer $FINA_SECRET_KEY" ``` **GET /recipients — Response** ```json { "name": "María Pérez", "account": "2345678901" } ``` **POST /payouts — curl** ```bash curl -X POST https://fina.do/api/wallet/v1/payouts \ -H "Authorization: Bearer $FINA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "account": "2345678901", "amount_minor": 250000, "reference": "withdrawal-981", "description": "Retiro de tu saldo" }' ``` **POST /payouts — Response** ```json { "id": "0b6f8f6e-2f4c-4a63-9d1e-3c1b2a7d9e10", "status": "completed", "amount_minor": 250000, "currency_code": "DOP", "reference": "withdrawal-981", "recipient": { "name": "María Pérez" }, "created_at": "2026-10-08T15:04:05.000Z", "duplicate": false } ``` | Field | Rules | | --- | --- | | `account` | Required. Account number or phone, the same values a Fina user can send money to. | | `amount_minor` | Required. Integer > 0 in centavos. Debited from your wallet as is: there is no payout fee. | | `reference` | Required, up to 64 characters, unique per connection. | | `description` | Optional, up to 140 characters. The recipient sees it. | | `metadata` | Optional JSON object (up to 4 KB), stored with the payout. | #### Idempotency `reference` is unique per connection. Sending `POST /payouts` again with a reference that was already paid answers `200` with the original payout and `duplicate: true` — even if the body differs and even with simultaneous requests. Nothing is paid twice, so retry freely on timeouts with the same reference. #### Errors | HTTP | Code | Meaning | | --- | --- | --- | | 400 | `invalid_json`, `invalid_amount`, `reference_required`, `invalid_reference`, `invalid_description`, `invalid_metadata` | Fix the request. | | 400 | `same_account` | The recipient is the wallet of the connection owner. | | 403 | `payouts_disabled` | Payouts via API are off for this connection. | | 404 | `recipient_not_found` | No active Fina Pay wallet with that account number or phone. | | 409 | `insufficient_funds` | Your wallet balance is lower than `amount_minor`. Nothing moved. | | 409 | `currency_mismatch` | Your wallet and the recipient's use different currencies. | | 409 | `wallet_blocked` | Your wallet is blocked. Contact Fina support. | Full request and response schemas: [Fina Pay API reference](https://fina.do/developers/api-fina-pay.md). Source: https://fina.do/developers#payouts --- ## Webhooks > Signed IPN events (charge.* and subscription.*), signature verification in 3 languages, retries and idempotent handling. Set a webhook URL and an IPN secret on your connection. Fina then sends `POST` requests with `Content-Type: application/json` and the header `x-fina-sig` for each event. #### Charge events - `charge.paid` - `charge.expired` - `charge.cancelled` - `charge.refunded` **charge.paid — JSON** ```json { "event": "charge.paid", "charge_id": "550e8400-e29b-41d4-a716-446655440000", "reference": "order-42", "amount_minor": 150000, "fee_minor": 3750, "net_minor": 146250, "currency_code": "DOP", "status": "paid", "transfer_id": "8d2b…", "paid_at": "2026-10-05T14:03:11.000Z", "timestamp": "2026-10-05T14:03:12.000Z" } ``` #### Verify the signature `x-fina-sig` is the hex **HMAC-SHA512** of the payload serialized as `JSON.stringify(payload, Object.keys(payload).sort())` — compact JSON whose keys follow the sorted list of top-level keys — keyed with your IPN secret. Note that it is computed over that re-serialization, not over the raw body bytes: parse the body, rebuild the string and compare in constant time. The Python and PHP helpers below produce exactly the same bytes as the JavaScript call. **verifyFinaSignature — TypeScript** ```ts import { createHmac, timingSafeEqual } from 'node:crypto' /** Fina signs JSON.stringify(payload, sortedTopLevelKeys) with HMAC-SHA512. */ export function verifyFinaSignature(rawBody: string, signature: string, secret: string): boolean { const payload = JSON.parse(rawBody) const signed = JSON.stringify(payload, Object.keys(payload).sort()) const expected = createHmac('sha512', secret).update(signed).digest('hex') return expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature)) } ``` **verifyFinaSignature — Python** ```python import hashlib import hmac import json def fina_signed_string(payload: dict) -> str: """Same bytes as JS JSON.stringify(payload, Object.keys(payload).sort()).""" keys = sorted(payload.keys()) def keep(value): if isinstance(value, dict): return {k: keep(value[k]) for k in keys if k in value} if isinstance(value, list): return [keep(v) for v in value] return value return json.dumps(keep(payload), separators=(",", ":"), ensure_ascii=False) def verify_fina_signature(raw_body: bytes, signature: str, secret: str) -> bool: payload = json.loads(raw_body) expected = hmac.new(secret.encode(), fina_signed_string(payload).encode(), hashlib.sha512).hexdigest() return hmac.compare_digest(expected, signature or "") ``` **verifyFinaSignature — PHP** ```php $k = $keep($value->$k); } return $out; } return is_array($value) ? array_map($keep, $value) : $value; }; return json_encode($keep($payload), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } function verify_fina_signature(string $rawBody, string $signature, string $secret): bool { $payload = json_decode($rawBody); // objects, not arrays: keeps {} vs [] intact $expected = hash_hmac('sha512', fina_signed_string($payload), $secret); return hash_equals($expected, $signature); } ``` #### Delivery and retries - Answer any `2xx` within **15 seconds** to acknowledge. Do slow work (emails, fulfilment) after responding. - Anything else (timeout, `4xx`, `5xx`, network error) is retried after **1 min, 5 min, 30 min, 2 h and 24 h**; after that the delivery is marked failed. - You can see recent deliveries and their status in the Fina Pay connector of your business. - Events can arrive more than once and out of order. Make the handler idempotent (key on `charge_id` + `event`, or `event_id` for subscriptions) and, when in doubt, read the current state with `GET /charges/{id}`. #### Subscription events Merchants using Fina Pay subscriptions also receive `subscription.created`, `subscription.trial_will_end`, `subscription.renewed`, `subscription.payment_failed`, `subscription.past_due`, `subscription.cancel_scheduled`, `subscription.reactivated`, `subscription.paused`, `subscription.resumed`, `subscription.plan_changed`, `subscription.canceled`, `subscription.expired` and `mandate.revoked`, with the same header, signature and retries. See [Subscriptions](https://fina.do/developers/subscriptions.md). Source: https://fina.do/developers#webhooks --- ## Subscriptions > Recurring Fina Pay payments: hosted checkout, customer portal and lifecycle webhooks (rolling out). > **Note:** Fina Pay subscriptions are rolling out by business. Subscription webhooks to external URLs are enabled per platform setting; until then, plans are managed from Fina and the modules that use them (for example farm subscription boxes). A subscription is a mandate the customer authorizes once with their wallet PIN; Fina then charges each period automatically from their Fina Pay balance. - **Hosted checkout** — `https://fina.do/pay/s/` shows the plan terms, signs the customer in and asks for the PIN. It opens the Fina app when installed. Supports a `?return=` URL back to your site. - **Customer portal** — `https://fina.do/pay/subscriptions`, where customers see, cancel or reactivate their subscriptions and set a spending cap. - **Entitlement** — every webhook carries `entitled` and `entitled_until`: give access while `entitled` is true. Grace periods and trials are already applied. **subscription.renewed — JSON** ```json { "event": "subscription.renewed", "event_id": "0b6f…", "subscription_id": "a1c2…", "plan_code": "coffee-monthly", "status": "active", "cancel_at_period_end": false, "entitled": true, "entitled_until": "2026-11-05T14:00:00.000Z", "period": { "id": "…", "sequence": 2, "kind": "paid", "start": "…", "end": "…", "status": "paid", "amount_minor": 90000, "merchant_ref": null }, "charge": { "id": "…", "amount_minor": 90000, "fee_minor": 2250, "net_minor": 87750, "paid_at": "…" }, "external_customer_ref": "customer-981", "timestamp": "2026-10-05T14:00:02.000Z" } ``` > **Warning:** Signature caveat for subscription events: because the signed string uses the top-level key list as a JSON.stringify replacer, nested objects (`period`, `charge`) contribute only keys that also exist at the top level. Use the helpers in [Webhooks](https://fina.do/developers/webhooks.md) as-is — they reproduce this exactly — and re-read sensitive nested values from Fina when they drive money decisions. Source: https://fina.do/developers#subscriptions --- ## Storefront APIs > Build your own customer app or website on top of a Fina business: app key, envelope, /config, delivery quotes and weight-based shipping. The official white-label apps of Fina businesses are built on these APIs, and you can use them too. Each API is scoped to **one business**: the app key identifies it, and no endpoint lists or discovers other businesses. | Vertical | Base path | App key header | | --- | --- | --- | | [Farm storefront API](https://fina.do/developers/api-farm.md) | `/api/farm/v1` | `X-Farm-App-Key` | | [Boutique storefront API](https://fina.do/developers/api-boutique.md) | `/api/boutique/v1` | `X-Boutique-App-Key` | | [Restaurant storefront API](https://fina.do/developers/api-restaurant.md) | `/api/restaurant/v1` | `X-Restaurant-App-Key` | | [Barbershop booking API](https://fina.do/developers/api-barbershop.md) | `/api/barbershop/v1` | `X-Barbershop-App-Key` | | [Real estate API](https://fina.do/developers/api-real-estate.md) | `/api/real-estate/v1` | `X-Real-Estate-App-Key` | | [Rent a car API](https://fina.do/developers/api-rent-car.md) | `/api/rent-car/v1` | `X-Rent-Car-App-Key` | #### Authentication - **App key** — created by the business owner in their panel, in the module's customer-app section. It is an identifier and rate-limit scope, not a secret: it ships inside the app. - **Customer token** — private routes (`/me`, `/orders`, `/addresses`…) also need `Authorization: Bearer ` of the customer's Fina account (Supabase Auth JWT). Public routes (`/config`, `/catalog`, `/windows`, slots…) work with the app key alone. #### Response envelope Farm, boutique, restaurant and barbershop answer `{ "data": …, "error": null, "request_id": "…" }`; on failure `data` is `null` and `error` is `{ "code", "message", "details"? }` with a stable upper-case `code` (`RATE_LIMITED`, `OUT_OF_DELIVERY_ZONE`, `SHIPMENT_LIMIT_EXCEEDED`…). Every response has an `X-Request-Id` header — include it when contacting support. **First call — curl** ```bash curl https://fina.do/api/boutique/v1/config \ -H "X-Boutique-App-Key: $APP_KEY" ``` **First call — TypeScript** ```ts const res = await fetch('https://fina.do/api/boutique/v1/config', { headers: { 'X-Boutique-App-Key': process.env.APP_KEY! }, }) const { data, error, request_id } = await res.json() if (error) throw new Error(`${error.code} (request ${request_id})`) console.log(data.business.name) ``` #### Start with /config `GET /config` returns the business identity and everything the app needs to boot: fulfilment modes, delivery pricing (`delivery_fee_by_order_total`, `delivery_fee_by_weight` with its `weight_unit`), payment options (`card_enabled`, `cash_on_delivery`) and whether Fina Pay subscriptions are available. Cache it and refresh on app start. #### Delivery quotes and weight-based shipping `POST /delivery/estimate` with an address returns `fee_minor`, `distance_meters`, the delivery zone and `pricing_mode` (`flat`, `per_km`, `tiers`, `order_total`, `by_weight`). For weight-based shipping send the cart as `items: [{ product_id, variant_id?, quantity }]`: the response adds `weight` (real, volumetric and billable grams), `fee_by_weight` and the zone limits. If the cart exceeds every zone's maximum weight or size you get `deliverable: false` with `reason: "too_heavy" | "too_large"`, and creating the order answers `409 SHIPMENT_LIMIT_EXCEEDED`. #### Rate limits Per-minute limits per app key and client. Over the limit: `429` with `error.code = "RATE_LIMITED"` and `details.retry_after_seconds`. Source: https://fina.do/developers#storefront-apis --- ## Fina Pay API > Charges, payment links, payouts and signed webhooks for any store, app or backend. Charges, payment links, payouts and signed webhooks for any store, app or backend. | Field | Value | | --- | --- | | Base URL | `https://fina.do/api/wallet/v1` | | Authentication | Bearer secret key (`sk_live_…`) from the Fina Pay connector of your business | | OpenAPI 3.1 | [/api/wallet/v1/openapi.json](https://fina.do/api/wallet/v1/openapi.json) | | Built for | Merchants and developers integrating payments | Webhooks are part of the spec too (`webhooks` in OpenAPI 3.1). Guides: [Quickstart](https://fina.do/developers/quickstart.md), [Charges](https://fina.do/developers/charges.md), [Webhooks](https://fina.do/developers/webhooks.md). #### Endpoints Base URL: `https://fina.do/api/wallet/v1` | Method | Path | Summary | Auth | | --- | --- | --- | --- | | GET | `/charges` | Look up a charge by payment token (public key) | Public | | POST | `/charges` | Create a charge and get its payment URL | Secret key | | GET | `/charges/{id}` | Get a charge (poll its status, fee and net) | Secret key | | POST | `/charges/{id}/cancel` | Cancel a pending charge | Secret key | | POST | `/payouts` | Pay a Fina Pay user from your wallet | Secret key | | GET | `/payouts/{id}` | Get a payout of this connection | Secret key | | GET | `/recipients` | Look up who will receive a payout | Secret key | Source: https://fina.do/developers#api-fina-pay --- ## Farm storefront API > Catalog, delivery windows, orders, subscription boxes and delivery quotes of one farm. Catalog, delivery windows, orders, subscription boxes and delivery quotes of one farm. | Field | Value | | --- | --- | | Base URL | `https://fina.do/api/farm/v1` | | Authentication | `X-Farm-App-Key` + customer bearer token on private routes | | OpenAPI 3.1 | [/api/farm/v1/openapi.json](https://fina.do/api/farm/v1/openapi.json) | | Built for | Custom customer apps and websites for a farm | Shared rules (app key, envelope, /config, delivery quotes): [Storefront APIs](https://fina.do/developers/storefront-apis.md). #### Endpoints Base URL: `https://fina.do/api/farm/v1` | Method | Path | Summary | Auth | | --- | --- | --- | --- | | GET | `/config` | Identidad de la granja y lo que la app necesita para arrancar | App key | | GET | `/locations` | Sucursales activas donde se puede recoger | App key | | GET | `/catalog` | Catálogo con unidad de venta y precio por pieza derivado | App key | | GET | `/plans` | Planes con su precio, su ahorro por ciclo y por pieza | App key | | GET | `/windows` | Franjas de una fecha para una modalidad | App key | | GET | `/me` | Perfil del cliente en esta granja | App key + Customer token | | PATCH | `/me` | Editar nombre y teléfono | App key + Customer token | | DELETE | `/me` | Baja de cuenta (exige contraseña o código al correo) | App key + Customer token | | POST | `/me/delete-code` | Enviar al correo el código para confirmar la baja | App key + Customer token | | GET | `/addresses` | Libreta de direcciones | App key + Customer token | | POST | `/addresses` | Guardar una dirección | App key + Customer token | | PATCH | `/addresses/{id}` | Editar una dirección | App key + Customer token | | DELETE | `/addresses/{id}` | Borrar una dirección | App key + Customer token | | GET | `/geocode/reverse` | Dirección escrita de un punto («usar mi ubicación») | App key | | POST | `/delivery/estimate` | Costo de envío a una dirección | App key + Customer token | | POST | `/discount-codes/validate` | Vista previa de un cupón: `{ code, subtotal_minor }` → `{ valid, discount_minor, … }` | App key | | GET | `/orders` | Pedidos del cliente | App key + Customer token | | POST | `/orders` | Crear un pedido | App key + Customer token | | GET | `/orders/{id}` | Detalle y seguimiento de un pedido | App key + Customer token | | POST | `/orders/{id}/cancel` | Cancelar un pedido | App key + Customer token | | GET | `/tracking` | Pedidos en curso | App key + Customer token | | GET | `/subscriptions` | Planes del cliente | App key + Customer token | | POST | `/subscriptions` | Contratar un plan | App key + Customer token | | GET | `/subscriptions/{id}` | Detalle con sus ciclos | App key + Customer token | | PATCH | `/subscriptions/{id}` | Cambiar día, franja, destino o medio de pago | App key + Customer token | | DELETE | `/subscriptions/{id}` | Cancelar el plan | App key + Customer token | | POST | `/subscriptions/{id}/pause` | Pausar | App key + Customer token | | POST | `/subscriptions/{id}/resume` | Reanudar | App key + Customer token | | POST | `/subscriptions/{id}/skip` | Saltar la entrega siguiente | App key + Customer token | | POST | `/subscriptions/fina-pay/checkout` | Checkout de Fina Pay para pagar un plan | App key + Customer token | | GET | `/subscriptions/fina-pay/{id}` | Estado del pago con Fina Pay de un plan | App key + Customer token | | GET | `/payment-methods` | Tarjetas guardadas | App key + Customer token | | DELETE | `/payment-methods/{id}` | Borrar una tarjeta | App key + Customer token | | GET | `/announcements/active` | Anuncios vigentes del negocio para quien abre la app | App key + Customer token (or App key) | | POST | `/announcements/{id}/events` | Registrar que un anuncio se vio, se tocó un botón o se cerró | App key + Customer token (or App key) | | GET | `/loyalty` | Programa de fidelidad, recompensas y tarjeta del cliente | App key + Customer token (or App key) | | POST | `/loyalty/redemptions` | Canjear una recompensa con puntos | App key + Customer token | | POST | `/loyalty/redemptions/{id}/cancel` | Cancelar un canje propio pendiente | App key + Customer token | | POST | `/loyalty/check-in` | Check-in de autoservicio con el QR del mostrador | App key + Customer token | | GET | `/network/application` | Mi solicitud para vender a través de este negocio | App key + Customer token | | POST | `/network/application` | Enviar «Vende con nosotros» | App key + Customer token | | GET | `/advisory` | Advisory service, request form and my cases | App key + Customer token (or App key) | | POST | `/advisory/requests` | Request advisory | App key + Customer token | | GET | `/advisory/cases/{id}` | Case detail | App key + Customer token | | GET | `/advisory/cases/{id}/activities` | Case timeline visible to the client | App key + Customer token | | POST | `/advisory/cases/{id}/messages` | Send a message to the advisor | App key + Customer token | | GET | `/advisory/cases/{id}/dossiers` | Document folders of the case | App key + Customer token | | POST | `/advisory/uploads` | Upload a file (raw bytes) | App key + Customer token | | POST | `/advisory/dossier-items/{id}/files` | Attach an uploaded file to a document | App key + Customer token | | DELETE | `/advisory/files/{id}` | Delete one of my files | App key + Customer token | | GET | `/advisory/files/{id}/url` | Signed URL of a file | App key + Customer token | | GET | `/advisory/cases/{id}/visits` | Visits of the case | App key + Customer token | | POST | `/advisory/cases/{id}/visit-requests` | Request a visit | App key + Customer token | | GET | `/advisory/visits/{id}/report-pdf` | Signed URL of the published report PDF | App key + Customer token | | POST | `/advisory/visits/{id}/cancel` | Cancel a visit | App key + Customer token | | POST | `/advisory/visits/{id}/acknowledge` | Acknowledge a published visit report | App key + Customer token | | GET | `/advisory/cases/{id}/metrics` | Indicators of the case with their series | App key + Customer token | | POST | `/advisory/metrics/{id}/records` | Record a measurement | App key + Customer token | | GET | `/advisory/cases/{id}/market` | My offers and the opportunities shared with me | App key + Customer token | | POST | `/advisory/cases/{id}/offers` | Propose an offer | App key + Customer token | | POST | `/advisory/opportunities/{id}/respond` | Respond to a shared opportunity | App key + Customer token | | GET | `/blog` | Datos del blog del negocio | App key + Customer token (or App key) | | GET | `/blog/posts` | Posts publicados (sin cuerpo) | App key + Customer token (or App key) | | GET | `/blog/posts/{id}` | Un post con su cuerpo según el acceso del cliente | App key + Customer token (or App key) | | POST | `/blog/posts/{id}/purchase` | Comprar un post de pago con Fina Pay | App key + Customer token | | POST | `/blog/posts/{id}/tips` | Dejar una propina con Fina Pay | App key + Customer token (or App key) | | POST | `/blog/membership/checkout` | Unirse a la membresía del blog (checkout de suscripción) | App key + Customer token | | POST | `/devices` | Registrar el token push del dispositivo | App key + Customer token | | POST | `/devices/ping` | Enviar un aviso de prueba a mis dispositivos | App key + Customer token | | DELETE | `/devices/{id}` | Revocar el token | App key + Customer token | | GET | `/me/pricing` | Mayoreo / B2B: los precios de este cliente | App key + Customer token | | GET | `/quotes` | Mayoreo / B2B: cotizaciones del cliente | App key + Customer token | | POST | `/quotes/request` | Mayoreo / B2B: pedir una cotización | App key + Customer token | | GET | `/quotes/{id}` | Mayoreo / B2B: detalle de una cotización | App key + Customer token | | POST | `/quotes/{id}/accept` | Mayoreo / B2B: aceptar una cotización (crea el pedido) | App key + Customer token | | POST | `/quotes/{id}/decline` | Mayoreo / B2B: rechazar una cotización | App key + Customer token | Source: https://fina.do/developers#api-farm --- ## Boutique storefront API > Products with variants, addresses, delivery quotes (incl. weight-based shipping) and orders of one store. Products with variants, addresses, delivery quotes (incl. weight-based shipping) and orders of one store. | Field | Value | | --- | --- | | Base URL | `https://fina.do/api/boutique/v1` | | Authentication | `X-Boutique-App-Key` + customer bearer token on private routes | | OpenAPI 3.1 | [/api/boutique/v1/openapi.json](https://fina.do/api/boutique/v1/openapi.json) | | Built for | Custom customer apps and websites for a store | Shared rules (app key, envelope, /config, delivery quotes): [Storefront APIs](https://fina.do/developers/storefront-apis.md). #### Endpoints Base URL: `https://fina.do/api/boutique/v1` | Method | Path | Summary | Auth | | --- | --- | --- | --- | | GET | `/config` | Identidad de la granja y lo que la app necesita para arrancar | App key | | GET | `/locations` | Sucursales activas donde se puede recoger | App key | | GET | `/catalog` | Catálogo con unidad de venta y precio por pieza derivado | App key | | GET | `/windows` | Franjas de una fecha para una modalidad | App key | | GET | `/me` | Perfil del cliente en esta granja | App key + Customer token | | PATCH | `/me` | Editar nombre y teléfono | App key + Customer token | | DELETE | `/me` | Baja de cuenta | App key + Customer token | | POST | `/me/delete-code` | Enviar al correo el código para confirmar la baja | App key + Customer token | | GET | `/addresses` | Libreta de direcciones | App key + Customer token | | POST | `/addresses` | Guardar una dirección | App key + Customer token | | PATCH | `/addresses/{id}` | Editar una dirección | App key + Customer token | | DELETE | `/addresses/{id}` | Borrar una dirección | App key + Customer token | | GET | `/geocode/reverse` | Dirección escrita de un punto («usar mi ubicación») | App key | | POST | `/delivery/estimate` | Costo de envío a una dirección | App key + Customer token | | POST | `/discount-codes/validate` | Vista previa de un cupón: `{ code, subtotal_minor }` → `{ valid, discount_minor, … }` | App key | | GET | `/orders` | Pedidos del cliente | App key + Customer token | | POST | `/orders` | Crear un pedido | App key + Customer token | | GET | `/orders/{id}` | Detalle y seguimiento de un pedido | App key + Customer token | | POST | `/orders/{id}/cancel` | Cancelar un pedido | App key + Customer token | | GET | `/tracking` | Pedidos en curso | App key + Customer token | | GET | `/payment-methods` | Tarjetas guardadas | App key + Customer token | | DELETE | `/payment-methods/{id}` | Borrar una tarjeta | App key + Customer token | | GET | `/announcements/active` | Anuncios vigentes del negocio para quien abre la app | App key + Customer token (or App key) | | POST | `/announcements/{id}/events` | Registrar que un anuncio se vio, se tocó un botón o se cerró | App key + Customer token (or App key) | | GET | `/loyalty` | Programa de fidelidad, recompensas y tarjeta del cliente | App key + Customer token (or App key) | | POST | `/loyalty/redemptions` | Canjear una recompensa con puntos | App key + Customer token | | POST | `/loyalty/redemptions/{id}/cancel` | Cancelar un canje propio pendiente | App key + Customer token | | POST | `/loyalty/check-in` | Check-in de autoservicio con el QR del mostrador | App key + Customer token | | GET | `/network/application` | Mi solicitud para vender a través de este negocio | App key + Customer token | | POST | `/network/application` | Enviar «Vende con nosotros» | App key + Customer token | | GET | `/advisory` | Advisory service, request form and my cases | App key + Customer token (or App key) | | POST | `/advisory/requests` | Request advisory | App key + Customer token | | GET | `/advisory/cases/{id}` | Case detail | App key + Customer token | | GET | `/advisory/cases/{id}/activities` | Case timeline visible to the client | App key + Customer token | | POST | `/advisory/cases/{id}/messages` | Send a message to the advisor | App key + Customer token | | GET | `/advisory/cases/{id}/dossiers` | Document folders of the case | App key + Customer token | | POST | `/advisory/uploads` | Upload a file (raw bytes) | App key + Customer token | | POST | `/advisory/dossier-items/{id}/files` | Attach an uploaded file to a document | App key + Customer token | | DELETE | `/advisory/files/{id}` | Delete one of my files | App key + Customer token | | GET | `/advisory/files/{id}/url` | Signed URL of a file | App key + Customer token | | GET | `/advisory/cases/{id}/visits` | Visits of the case | App key + Customer token | | POST | `/advisory/cases/{id}/visit-requests` | Request a visit | App key + Customer token | | GET | `/advisory/visits/{id}/report-pdf` | Signed URL of the published report PDF | App key + Customer token | | POST | `/advisory/visits/{id}/cancel` | Cancel a visit | App key + Customer token | | POST | `/advisory/visits/{id}/acknowledge` | Acknowledge a published visit report | App key + Customer token | | GET | `/advisory/cases/{id}/metrics` | Indicators of the case with their series | App key + Customer token | | POST | `/advisory/metrics/{id}/records` | Record a measurement | App key + Customer token | | GET | `/advisory/cases/{id}/market` | My offers and the opportunities shared with me | App key + Customer token | | POST | `/advisory/cases/{id}/offers` | Propose an offer | App key + Customer token | | POST | `/advisory/opportunities/{id}/respond` | Respond to a shared opportunity | App key + Customer token | | GET | `/blog` | Datos del blog del negocio | App key + Customer token (or App key) | | GET | `/blog/posts` | Posts publicados (sin cuerpo) | App key + Customer token (or App key) | | GET | `/blog/posts/{id}` | Un post con su cuerpo según el acceso del cliente | App key + Customer token (or App key) | | POST | `/blog/posts/{id}/purchase` | Comprar un post de pago con Fina Pay | App key + Customer token | | POST | `/blog/posts/{id}/tips` | Dejar una propina con Fina Pay | App key + Customer token (or App key) | | POST | `/blog/membership/checkout` | Unirse a la membresía del blog (checkout de suscripción) | App key + Customer token | | POST | `/devices` | Registrar el token push del dispositivo | App key + Customer token | | POST | `/devices/ping` | Enviar un aviso de prueba a mis dispositivos | App key + Customer token | | DELETE | `/devices/{id}` | Revocar el token | App key + Customer token | | GET | `/me/pricing` | Mayoreo / B2B: los precios de este cliente | App key + Customer token | | GET | `/quotes` | Mayoreo / B2B: cotizaciones del cliente | App key + Customer token | | POST | `/quotes/request` | Mayoreo / B2B: pedir una cotización | App key + Customer token | | GET | `/quotes/{id}` | Mayoreo / B2B: detalle de una cotización | App key + Customer token | | POST | `/quotes/{id}/accept` | Mayoreo / B2B: aceptar una cotización (crea el pedido) | App key + Customer token | | POST | `/quotes/{id}/decline` | Mayoreo / B2B: rechazar una cotización | App key + Customer token | | GET | `/storefront` | Escaparate por bloques de la tienda (documento v2 publicado, ya proyectado) | App key | | POST | `/storefront/events` | Analíticas de la tienda en lotes (sin datos personales) | App key | | GET | `/products/{id}/reviews` | Reseñas publicadas de un producto (paginadas) | App key | | POST | `/reviews/photo-upload` | URL firmada para subir UNA foto de reseña | App key + Customer token | | POST | `/orders/{id}/items/{itemId}/review` | Calificar una línea de un pedido entregado | App key + Customer token | | PATCH | `/reviews/{id}` | Editar la reseña propia (una vez, en 7 días) | App key + Customer token | | GET | `/me/reviewables` | Lo que la cuenta tiene por calificar y sus reseñas | App key + Customer token | Source: https://fina.do/developers#api-boutique --- ## Restaurant storefront API > Menu, table QR ordering, waiter calls, table reservations, delivery and pickup orders of one restaurant. Menu, table QR ordering, waiter calls, table reservations, delivery and pickup orders of one restaurant. | Field | Value | | --- | --- | | Base URL | `https://fina.do/api/restaurant/v1` | | Authentication | `X-Restaurant-App-Key` + customer bearer token on private routes | | OpenAPI 3.1 | [/api/restaurant/v1/openapi.json](https://fina.do/api/restaurant/v1/openapi.json) | | Built for | Custom customer apps and websites for a restaurant | Shared rules (app key, envelope, /config, delivery quotes): [Storefront APIs](https://fina.do/developers/storefront-apis.md). #### Endpoints Base URL: `https://fina.do/api/restaurant/v1` | Method | Path | Summary | Auth | | --- | --- | --- | --- | | GET | `/config` | Identidad de la granja y lo que la app necesita para arrancar | App key | | GET | `/locations` | Sucursales activas donde se puede recoger | App key | | GET | `/catalog` | La carta: categorías del menú y platos con sus tamaños, extras y alérgenos | App key | | GET | `/plans` | Planes del restaurante: «Te lo llevamos» y «Úsalo cuando quieras» (sin sesión) | App key | | GET | `/windows` | Franjas de una fecha para una modalidad | App key | | GET | `/me` | Perfil del cliente en esta granja | App key + Customer token | | PATCH | `/me` | Editar nombre y teléfono | App key + Customer token | | DELETE | `/me` | Baja de cuenta | App key + Customer token | | GET | `/addresses` | Libreta de direcciones | App key + Customer token | | POST | `/addresses` | Guardar una dirección | App key + Customer token | | PATCH | `/addresses/{id}` | Editar una dirección | App key + Customer token | | DELETE | `/addresses/{id}` | Borrar una dirección | App key + Customer token | | GET | `/geocode/reverse` | Dirección escrita de un punto («usar mi ubicación») | App key | | POST | `/delivery/estimate` | Costo de envío a una dirección | App key + Customer token | | POST | `/discount-codes/validate` | Vista previa de un cupón: `{ code, subtotal_minor }` → `{ valid, discount_minor, … }` | App key | | GET | `/orders` | Pedidos del cliente | App key + Customer token | | POST | `/orders` | Crear un pedido | App key + Customer token | | GET | `/orders/{id}` | Detalle y seguimiento de un pedido | App key + Customer token | | POST | `/orders/{id}/cancel` | Cancelar un pedido | App key + Customer token | | GET | `/tracking` | Pedidos en curso | App key + Customer token | | GET | `/subscriptions` | Planes del cliente | App key + Customer token | | POST | `/subscriptions` | Contratar un plan | App key + Customer token | | GET | `/subscriptions/{id}` | Un plan del cliente con sus créditos del ciclo y sus ciclos | App key + Customer token | | DELETE | `/subscriptions/{id}` | Cancelar el plan | App key + Customer token | | POST | `/subscriptions/{id}/pause` | Pausar (los créditos ya dados siguen valiendo hasta que caduquen) | App key + Customer token | | POST | `/subscriptions/{id}/resume` | Reanudar | App key + Customer token | | POST | `/subscriptions/{id}/skip` | Saltar la entrega (o renovación) siguiente | App key + Customer token | | POST | `/subscriptions/fina-pay/checkout` | Checkout de Fina Pay para pagar un plan | App key + Customer token | | GET | `/subscriptions/fina-pay/{id}` | Estado del pago con Fina Pay de un plan | App key + Customer token | | GET | `/payment-methods` | Tarjetas guardadas | App key + Customer token | | DELETE | `/payment-methods/{id}` | Borrar una tarjeta | App key + Customer token | | GET | `/announcements/active` | Anuncios vigentes del negocio para quien abre la app | App key + Customer token (or App key) | | POST | `/announcements/{id}/events` | Registrar que un anuncio se vio, se tocó un botón o se cerró | App key + Customer token (or App key) | | GET | `/loyalty` | Programa de fidelidad, recompensas y tarjeta del cliente | App key + Customer token (or App key) | | POST | `/loyalty/redemptions` | Canjear una recompensa con puntos | App key + Customer token | | POST | `/loyalty/redemptions/{id}/cancel` | Cancelar un canje propio pendiente | App key + Customer token | | POST | `/loyalty/check-in` | Check-in de autoservicio con el QR del mostrador | App key + Customer token | | GET | `/advisory` | Advisory service, request form and my cases | App key + Customer token (or App key) | | POST | `/advisory/requests` | Request advisory | App key + Customer token | | GET | `/advisory/cases/{id}` | Case detail | App key + Customer token | | GET | `/advisory/cases/{id}/activities` | Case timeline visible to the client | App key + Customer token | | POST | `/advisory/cases/{id}/messages` | Send a message to the advisor | App key + Customer token | | GET | `/advisory/cases/{id}/dossiers` | Document folders of the case | App key + Customer token | | POST | `/advisory/uploads` | Upload a file (raw bytes) | App key + Customer token | | POST | `/advisory/dossier-items/{id}/files` | Attach an uploaded file to a document | App key + Customer token | | DELETE | `/advisory/files/{id}` | Delete one of my files | App key + Customer token | | GET | `/advisory/files/{id}/url` | Signed URL of a file | App key + Customer token | | GET | `/advisory/cases/{id}/visits` | Visits of the case | App key + Customer token | | POST | `/advisory/cases/{id}/visit-requests` | Request a visit | App key + Customer token | | GET | `/advisory/visits/{id}/report-pdf` | Signed URL of the published report PDF | App key + Customer token | | POST | `/advisory/visits/{id}/cancel` | Cancel a visit | App key + Customer token | | POST | `/advisory/visits/{id}/acknowledge` | Acknowledge a published visit report | App key + Customer token | | GET | `/advisory/cases/{id}/metrics` | Indicators of the case with their series | App key + Customer token | | POST | `/advisory/metrics/{id}/records` | Record a measurement | App key + Customer token | | GET | `/advisory/cases/{id}/market` | My offers and the opportunities shared with me | App key + Customer token | | POST | `/advisory/cases/{id}/offers` | Propose an offer | App key + Customer token | | POST | `/advisory/opportunities/{id}/respond` | Respond to a shared opportunity | App key + Customer token | | GET | `/blog` | Datos del blog del negocio | App key + Customer token (or App key) | | GET | `/blog/posts` | Posts publicados (sin cuerpo) | App key + Customer token (or App key) | | GET | `/blog/posts/{id}` | Un post con su cuerpo según el acceso del cliente | App key + Customer token (or App key) | | POST | `/blog/posts/{id}/purchase` | Comprar un post de pago con Fina Pay | App key + Customer token | | POST | `/blog/posts/{id}/tips` | Dejar una propina con Fina Pay | App key + Customer token (or App key) | | POST | `/blog/membership/checkout` | Unirse a la membresía del blog (checkout de suscripción) | App key + Customer token | | POST | `/devices` | Registrar el token push del dispositivo | App key + Customer token | | POST | `/devices/ping` | Enviar un aviso de prueba a mis dispositivos | App key + Customer token | | DELETE | `/devices/{id}` | Revocar el token | App key + Customer token | | GET | `/me/pricing` | Mayoreo / B2B: los precios de este cliente | App key + Customer token | | GET | `/quotes` | Mayoreo / B2B: cotizaciones del cliente | App key + Customer token | | POST | `/quotes/request` | Mayoreo / B2B: pedir una cotización | App key + Customer token | | GET | `/quotes/{id}` | Mayoreo / B2B: detalle de una cotización | App key + Customer token | | POST | `/quotes/{id}/accept` | Mayoreo / B2B: aceptar una cotización (crea el pedido) | App key + Customer token | | POST | `/quotes/{id}/decline` | Mayoreo / B2B: rechazar una cotización | App key + Customer token | | POST | `/ar-events` | Eventos anónimos de «Platos en AR» (vista, giro, lanzar AR, colocado, fallo, añadir al pedido) | App key | | GET | `/tables/{token}` | La mesa detrás de un QR de este restaurante | App key | | GET | `/tables/{token}/calls` | Cómo van las llamadas de la mesa (abierta o atendida) | App key | | POST | `/tables/{token}/calls` | «Llamar al mesero» (`waiter`) o «Pedir la cuenta» (`bill`) | App key | | GET | `/status` | Estado en vivo: `{ busy: { active, extra_min }, open: { configured, is_open, closes_at, opens_at, closing_soon } }` | App key | | GET | `/reservations/info` | ¿Acepta reservas de mesa desde la app? Reglas: días hacia adelante, duración, grupo máximo y zonas elegibles | App key | | GET | `/reservations/slots` | Horas libres de un día para un grupo (minutos desde medianoche, hora de RD) | App key | | GET | `/reservations` | Mis reservas (requiere sesión) | App key | | POST | `/reservations` | Reservar una mesa: `{ date, start_min, party_size, notes?, phone?, zone_id? }` | App key | | POST | `/reservations/{id}/cancel` | Cancelar una reserva propia que aún no empezó | App key | | GET | `/tables/{token}/orders` | Pedidos abiertos de la mesa (de la app o del mesero) y su avance en cocina | App key | | GET | `/credits` | Créditos vigentes del cliente, los que caducan antes primero | App key + Customer token | | GET | `/tables/{token}/round` | La ronda abierta de la mesa (o la enviada hace < 10 min), o `null` | App key | | POST | `/tables/{token}/round` | Abre la ronda o me une a la abierta (idempotente): `{ name? }` | App key + Customer token | | PUT | `/tables/{token}/round/me` | Reemplaza MIS líneas y mi estado: `{ items, ready?, name? }` | App key + Customer token | | DELETE | `/tables/{token}/round/me` | Salgo de la ronda (mis líneas se van) | App key + Customer token | | POST | `/tables/{token}/round/submit` | Envía la ronda a la cocina: UN pedido para toda la mesa | App key + Customer token | | POST | `/uploads/sign` | Permiso para subir una foto de referencia del pedido | App key + Customer token | Source: https://fina.do/developers#api-restaurant --- ## Barbershop booking API > Services, staff, live slots, appointments, waitlist and reviews of one barbershop. Services, staff, live slots, appointments, waitlist and reviews of one barbershop. | Field | Value | | --- | --- | | Base URL | `https://fina.do/api/barbershop/v1` | | Authentication | `X-Barbershop-App-Key` + customer bearer token on private routes | | OpenAPI 3.1 | [/api/barbershop/v1/openapi.json](https://fina.do/api/barbershop/v1/openapi.json) | | Built for | Custom booking apps for a barbershop or salon | Shared rules (app key, envelope, /config, delivery quotes): [Storefront APIs](https://fina.do/developers/storefront-apis.md). #### Endpoints Base URL: `https://fina.do/api/barbershop/v1` | Method | Path | Summary | Auth | | --- | --- | --- | --- | | GET | `/config` | Get the configured business and booking policy | App key | | GET | `/catalog` | Get appointment services, staff, and public reviews | App key | | GET | `/slots` | Calculate currently available appointment starts | App key | | POST | `/discounts/validate` | Preview a discount for the selected appointment items | App key | | POST | `/me/link` | Link the global account to this business customer record | App key + Customer token | | GET | `/me` | Get the linked profile for this business | App key + Customer token | | PATCH | `/me` | Update name or phone on the linked business profile | App key + Customer token | | DELETE | `/me` | Delete the customer account and personal data | App key + Customer token | | GET | `/appointments` | List appointments belonging to the linked customer | App key + Customer token | | POST | `/appointments` | Create an appointment atomically and idempotently | App key + Customer token (or App key) | | GET | `/appointments/{id}` | Get one appointment belonging to the linked customer | App key + Customer token | | POST | `/appointments/{id}/cancel` | Cancel an appointment under the configured policy | App key + Customer token | | GET | `/appointments/{id}/reschedule` | Get valid replacement starts for an appointment | App key + Customer token | | POST | `/appointments/{id}/reschedule` | Move an appointment to a currently valid slot | App key + Customer token | | POST | `/appointments/{id}/review` | Review a completed appointment | App key + Customer token | | GET | `/waitlist` | List the linked customer waitlist entries | App key + Customer token | | POST | `/waitlist` | Join the waitlist for an appointment service | App key + Customer token | | DELETE | `/waitlist/{id}` | Cancel one waiting entry belonging to the linked customer | App key + Customer token | | GET | `/standing/plans` | List the plans a customer can subscribe to | App key | | POST | `/standing/preview` | Preview the dates a standing appointment would get | App key | | GET | `/standing` | List the customer's standing appointments | App key + Customer token | | POST | `/standing` | Subscribe to a standing appointment and get the Fina Pay checkout | App key + Customer token | | GET | `/standing/{id}` | Get payment state, upcoming appointments and skipped dates | App key + Customer token | | POST | `/standing/{id}/cancel` | Cancel a standing appointment | App key + Customer token | | GET | `/announcements/active` | Live announcements from the business for this customer | App key + Customer token (or App key) | | POST | `/announcements/{id}/events` | Record that an announcement was seen, a button was tapped or it was closed | App key + Customer token (or App key) | | GET | `/loyalty` | Loyalty program, rewards and the customer card | App key + Customer token (or App key) | | POST | `/loyalty/redemptions` | Redeem a reward with points | App key + Customer token | | POST | `/loyalty/redemptions/{id}/cancel` | Cancel one of my pending redemptions | App key + Customer token | | POST | `/loyalty/check-in` | Self check-in with the counter QR | App key + Customer token | | GET | `/advisory` | Advisory service, request form and my cases | App key + Customer token (or App key) | | POST | `/advisory/requests` | Request advisory | App key + Customer token | | GET | `/advisory/cases/{id}` | Case detail | App key + Customer token | | GET | `/advisory/cases/{id}/activities` | Case timeline visible to the client | App key + Customer token | | POST | `/advisory/cases/{id}/messages` | Send a message to the advisor | App key + Customer token | | GET | `/advisory/cases/{id}/dossiers` | Document folders of the case | App key + Customer token | | POST | `/advisory/uploads` | Upload a file (raw bytes) | App key + Customer token | | POST | `/advisory/dossier-items/{id}/files` | Attach an uploaded file to a document | App key + Customer token | | DELETE | `/advisory/files/{id}` | Delete one of my files | App key + Customer token | | GET | `/advisory/files/{id}/url` | Signed URL of a file | App key + Customer token | | GET | `/advisory/cases/{id}/visits` | Visits of the case | App key + Customer token | | POST | `/advisory/cases/{id}/visit-requests` | Request a visit | App key + Customer token | | GET | `/advisory/visits/{id}/report-pdf` | Signed URL of the published report PDF | App key + Customer token | | POST | `/advisory/visits/{id}/cancel` | Cancel a visit | App key + Customer token | | POST | `/advisory/visits/{id}/acknowledge` | Acknowledge a published visit report | App key + Customer token | | GET | `/advisory/cases/{id}/metrics` | Indicators of the case with their series | App key + Customer token | | POST | `/advisory/metrics/{id}/records` | Record a measurement | App key + Customer token | | GET | `/advisory/cases/{id}/market` | My offers and the opportunities shared with me | App key + Customer token | | POST | `/advisory/cases/{id}/offers` | Propose an offer | App key + Customer token | | POST | `/advisory/opportunities/{id}/respond` | Respond to a shared opportunity | App key + Customer token | | GET | `/blog` | Blog header, membership, tips and tags | App key + Customer token (or App key) | | GET | `/blog/posts` | Published posts (cards, no body) | App key + Customer token (or App key) | | GET | `/blog/posts/{id}` | One post with the body the customer may read | App key + Customer token (or App key) | | POST | `/blog/posts/{id}/purchase` | Buy a paid post with Fina Pay | App key + Customer token | | POST | `/blog/posts/{id}/tips` | Leave a tip with Fina Pay | App key + Customer token (or App key) | | POST | `/blog/membership/checkout` | Join the blog membership (Fina Pay subscription checkout) | App key + Customer token | | POST | `/devices` | Register this installation and its current push token | App key + Customer token | | DELETE | `/devices` | Revoke this app installation for the signed-in account | App key + Customer token | Source: https://fina.do/developers#api-barbershop --- ## Real estate API > Published properties with filters and visit requests. Published properties with filters and visit requests. | Field | Value | | --- | --- | | Base URL | `https://fina.do/api/real-estate/v1` | | Authentication | `X-Real-Estate-App-Key` (+ customer bearer token to list or cancel own visits) | | OpenAPI 3.1 | [/api/real-estate/v1/openapi.json](https://fina.do/api/real-estate/v1/openapi.json) | | Built for | Property listing apps and websites | Shared rules (app key, envelope, /config, delivery quotes): [Storefront APIs](https://fina.do/developers/storefront-apis.md). #### Endpoints Base URL: `https://fina.do/api/real-estate/v1` | Method | Path | Summary | Auth | | --- | --- | --- | --- | | GET | `/config` | Get app configuration, business identity (business.vr: FinaVr \| null, the office tour), filter facets (including virtual_tours: true when any published property has a 360 tour), agents.enabled (true when at least one agent is visible) location.default_center ({ lat, lng } centroid of the published coordinates, or null) and the currencies. currency is the business currency (kept for compatibility); currencies is [{ code, symbol, minor_unit }] with the business currency first and every other currency used by a published property (format each price with ITS currency_code); facets.currencies lists the codes present in published properties; facets.price_range_minor is { rent, sale } normalized to the business currency with the latest rate of each currency (a currency without a rate is taken as is), and facets.price_range_minor_by_currency is the raw { USD: { rent, sale }, DOP: { rent, sale } } per currency (null when no price) | App key | | GET | `/catalog` | List published properties with filters (including agent_id and has_virtual_tour=true\|1; any other value is INVALID_REQUEST), sorting and cursor pagination. Prices may come in different currencies: min_price_minor/max_price_minor and sort=price_asc\|price_desc compare prices NORMALIZED to the business currency using the latest exchange rate of each currency (a currency without a rate is compared as is, unconverted). Each item carries currency_code, rent_price_minor/sale_price_minor in that currency and converted ({ currency_code: business currency, rent_price_minor, sale_price_minor, rate_as_of: YYYY-MM-DD } \| null; null when the property is already in the business currency or has no rate). Each item carries has_virtual_tour (boolean), has_vr (boolean, Fina VR tour or AR model) and agent { id, name, photo_url } \| null. With lat+lng every item also carries distance_m (integer meters, haversine to the published coordinate: the rounded one when the location is approximate; null without coordinates) | App key | | GET | `/properties/{id}` | Get a published property with gallery, features, location, nearby_places (kind + distance_m, ordered by distance), virtual_tour ({ url, provider: simple_virtual_tour } \| null, url is the 360 viewer to embed in a WebView; kept for compatibility), vr (FinaVr \| null: the same tour plus kind/cover/title and the AR model) its agent (full Agent object, or null to fall back to the business contact), currency_code with rent_price_minor/sale_price_minor in that currency, and converted ({ currency_code: business currency, rent_price_minor, sale_price_minor, rate_as_of } \| null) | App key | | GET | `/agents` | List the visible agents of the business, ordered by name: { agents: [{ id, name, photo_url, title, languages, properties_count }] } | App key | | GET | `/agents/{id}` | Get one visible agent (Agent schema). A missing, non-public, inactive or foreign agent answers 404 AGENT_NOT_FOUND | App key | | GET | `/visit-requests` | List the signed-in customer visit requests, each with the agent { id, name, photo_url, phone, whatsapp_phone } \| null that will contact the customer | Customer token | | POST | `/visit-requests` | Create a visit request (guest, or signed in when the business requires it) | App key | | DELETE | `/me` | Delete the signed-in customer account for this business. Returns { deleted, login_removed, pending_visits_kept } | Customer token | | POST | `/visit-requests/{id}/cancel` | Cancel a visit request of the signed-in customer | Customer token | Source: https://fina.do/developers#api-real-estate --- ## Rent a car API > Fleet catalog, availability, customer accounts, reservations with pickup or delivery, and Fina Pay payment. Fleet catalog, availability, customer accounts, reservations with pickup or delivery, and Fina Pay payment. | Field | Value | | --- | --- | | Base URL | `https://fina.do/api/rent-car/v1` | | Authentication | `X-Rent-Car-App-Key` (+ customer bearer token for account, reservations, addresses and devices) | | OpenAPI 3.1 | [/api/rent-car/v1/openapi.json](https://fina.do/api/rent-car/v1/openapi.json) | | Built for | Car rental apps and websites | Shared rules (app key, envelope, /config, delivery quotes): [Storefront APIs](https://fina.do/developers/storefront-apis.md). #### Endpoints Base URL: `https://fina.do/api/rent-car/v1` | Method | Path | Summary | Auth | | --- | --- | --- | --- | | GET | `/config` | Get app configuration | App key | | GET | `/catalog` | List rentable vehicles and prices, optionally filtered by make/model. Each vehicle carries has_vr (boolean) and vr (FinaVr \| null: 360 object / tour and AR model; the catalog is the vehicle detail). /config carries business.vr (the pickup office tour) | App key | | GET | `/vehicle-models` | List available vehicle makes and models | App key | | GET | `/availability` | Get monthly availability for a vehicle | App key | | GET | `/reservations` | My reservations (newest first, up to 50) | App key + Customer token | | POST | `/reservations` | Request a vehicle reservation | App key (or App key + Customer token) | | POST | `/reservations/quote` | Quote a reservation (no reservation is created) | App key (or App key + Customer token) | | GET | `/reservations/{id}` | Reservation detail | App key + Customer token | | POST | `/reservations/{id}/cancel` | Cancel my reservation | App key + Customer token | | GET | `/reservations/{id}/inspections` | Vehicle condition recorded by the business (pickup and return) | App key + Customer token | | POST | `/reservations/{id}/inspections/{kind}/acknowledge` | Confirm I reviewed the vehicle condition | App key + Customer token | | GET | `/reservations/{id}/payment` | Payment status of a reservation | App key + Customer token | | POST | `/reservations/{id}/extension/quote` | Quote an extension (return the vehicle later) | App key + Customer token | | POST | `/reservations/{id}/extension` | Request an extension | App key + Customer token | | POST | `/reservations/{id}/extension/cancel` | Withdraw my open extension | App key + Customer token | | PUT | `/me/driver` | Save my driver details | App key + Customer token | | DELETE | `/me/driver` | Delete my driver details and photos | App key + Customer token | | POST | `/uploads` | Upload a driver photo (license, ID card…) | App key + Customer token | | GET | `/uploads/{id}` | Preview URL of my photo | App key + Customer token | | DELETE | `/uploads/{id}` | Delete my photo | App key + Customer token | | GET | `/me` | Perfil del cliente en esta granja | appKey + Customer token | | PATCH | `/me` | Editar nombre y teléfono | appKey + Customer token | | DELETE | `/me` | Baja de cuenta | appKey + Customer token | | POST | `/me/delete-code` | Enviar al correo el código para confirmar la baja | appKey + Customer token | | GET | `/addresses` | Libreta de direcciones | appKey + Customer token | | POST | `/addresses` | Guardar una dirección | appKey + Customer token | | PATCH | `/addresses/{id}` | Editar una dirección | appKey + Customer token | | DELETE | `/addresses/{id}` | Borrar una dirección | appKey + Customer token | | GET | `/locations` | Sucursales activas donde se puede recoger | App key | | GET | `/geocode/reverse` | Dirección escrita de un punto («usar mi ubicación») | App key | | POST | `/delivery/estimate` | Costo de envío a una dirección | appKey + Customer token | | POST | `/devices` | Registrar el token push del dispositivo | appKey + Customer token | | POST | `/devices/ping` | Enviar un aviso de prueba a mis dispositivos | appKey + Customer token | | DELETE | `/devices/{id}` | Revocar el token | appKey + Customer token | | GET | `/announcements/active` | Anuncios vigentes del negocio para quien abre la app | App key + Customer token (or App key) | | POST | `/announcements/{id}/events` | Registrar que un anuncio se vio, se tocó un botón o se cerró | App key + Customer token (or App key) | | GET | `/blog` | Datos del blog del negocio | App key + Customer token (or App key) | | GET | `/blog/posts` | Posts publicados (sin cuerpo) | App key + Customer token (or App key) | | GET | `/blog/posts/{id}` | Un post con su cuerpo según el acceso del cliente | App key + Customer token (or App key) | | POST | `/blog/posts/{id}/purchase` | Comprar un post de pago con Fina Pay | appKey + Customer token | | POST | `/blog/posts/{id}/tips` | Dejar una propina con Fina Pay | App key + Customer token (or App key) | | POST | `/blog/membership/checkout` | Unirse a la membresía del blog (checkout de suscripción) | appKey + Customer token | | GET | `/loyalty` | Programa de fidelidad, recompensas y tarjeta del cliente | App key + Customer token (or App key) | | POST | `/loyalty/redemptions` | Canjear una recompensa con puntos | appKey + Customer token | | POST | `/loyalty/redemptions/{id}/cancel` | Cancelar un canje propio pendiente | appKey + Customer token | | POST | `/loyalty/check-in` | Check-in de autoservicio con el QR del mostrador | appKey + Customer token | | GET | `/advisory` | Advisory service, request form and my cases | App key + Customer token (or App key) | | POST | `/advisory/requests` | Request advisory | appKey + Customer token | | GET | `/advisory/cases/{id}` | Case detail | appKey + Customer token | | GET | `/advisory/cases/{id}/activities` | Case timeline visible to the client | appKey + Customer token | | POST | `/advisory/cases/{id}/messages` | Send a message to the advisor | appKey + Customer token | | GET | `/advisory/cases/{id}/dossiers` | Document folders of the case | appKey + Customer token | | POST | `/advisory/uploads` | Upload a file (raw bytes) | appKey + Customer token | | POST | `/advisory/dossier-items/{id}/files` | Attach an uploaded file to a document | appKey + Customer token | | DELETE | `/advisory/files/{id}` | Delete one of my files | appKey + Customer token | | GET | `/advisory/files/{id}/url` | Signed URL of a file | appKey + Customer token | | GET | `/advisory/cases/{id}/visits` | Visits of the case | appKey + Customer token | | POST | `/advisory/cases/{id}/visit-requests` | Request a visit | appKey + Customer token | | GET | `/advisory/visits/{id}/report-pdf` | Signed URL of the published report PDF | appKey + Customer token | | POST | `/advisory/visits/{id}/cancel` | Cancel a visit | appKey + Customer token | | POST | `/advisory/visits/{id}/acknowledge` | Acknowledge a published visit report | appKey + Customer token | | GET | `/advisory/cases/{id}/metrics` | Indicators of the case with their series | appKey + Customer token | | POST | `/advisory/metrics/{id}/records` | Record a measurement | appKey + Customer token | | GET | `/advisory/cases/{id}/market` | My offers and the opportunities shared with me | appKey + Customer token | | POST | `/advisory/cases/{id}/offers` | Propose an offer | appKey + Customer token | | POST | `/advisory/opportunities/{id}/respond` | Respond to a shared opportunity | appKey + Customer token | Source: https://fina.do/developers#api-rent-car --- ## Restaurant staff API > Kitchen queue, floor plan, cashier, deliveries and reservations for paired staff devices. Kitchen queue, floor plan, cashier, deliveries and reservations for paired staff devices. | Field | Value | | --- | --- | | Base URL | `https://fina.do/api/staff/v1` | | Authentication | `X-Staff-Device` (paired device) + `Authorization: Staff ` (PIN shift) | | OpenAPI 3.1 | [/api/staff/v1/openapi.json](https://fina.do/api/staff/v1/openapi.json) | | Built for | Kitchen displays, POS and waiter apps paired by the owner | Two factors: the business owner pairs a device from the panel (`POST /devices/pair` exchanges the pairing code for the device secret sent in `X-Staff-Device`), then each employee opens a shift with their PIN (`POST /sessions` → `Authorization: Staff `). Every shift route checks a role permission. #### Endpoints Base URL: `https://fina.do/api/staff/v1` | Method | Path | Summary | Auth | | --- | --- | --- | --- | | POST | `/devices/pair` | Canjea el código del panel y entrega el secreto del dispositivo | App key | | POST | `/devices/current/unpair` | Desvincula la tablet con el PIN de alguien con device.manage: revoca su secreto y cierra sus turnos | Device | | GET | `/roster` | Personal que puede entrar en este dispositivo. Cada persona trae `pin_length` (4, 5 o 6) para que el teclado pare ahí; null = PIN puesto antes de que guardáramos el largo | Device | | POST | `/sessions` | Abre un turno con el PIN | Device | | DELETE | `/sessions/current` | Cierra el turno | Device + Staff shift | | GET | `/me` | Empleado, rol, permisos y vistas. `business.business_type` (bakery, restaurant…) para el vocabulario de la app | Device + Staff shift | | GET | `/push-token` | Estado del token de este dispositivo: {registered, employee_id, is_me, platform, updated_at} | Device + Staff shift | | PUT | `/push-token` | Registra el token FCM de este dispositivo y lo ata a la persona del turno; cuerpo {token, platform: ios\|android, locale, app_version?} | Device + Staff shift | | DELETE | `/push-token` | Apaga los avisos de este dispositivo (no exige turno): {revoked} | Device | | GET | `/kitchen` | Cola de cocina. Panadería: cada línea trae `message` («Mensaje en la torta») y `reference_image_url` (foto firmada, ≤ 10 min) y el pedido `scheduled_for` + `window_start_min`/`window_end_min` si es un encargo. Cada pedido trae `payment` (ver `OrderPayment`) | Device + Staff shift | | POST | `/orders/{id}/advance` | Avanza pendiente → en cocina → listo | Device + Staff shift | | POST | `/orders/{id}/serve` | Marca servido un pedido listo | Device + Staff shift | | POST | `/orders/{id}/void` | Anula un pedido sin cobrar, con motivo | Device + Staff shift | | POST | `/orders/{id}/charge` | Cobra el pedido y registra el ingreso; cuerpo opcional {payment_method: cash\|card\|transfer\|other, cash_received_minor} | Device + Staff shift | | GET | `/cashier` | Pedidos pendientes de cobro (los de las últimas 36 h y los encargos programados desde ayer). Cada pedido trae `payment`: si `on_credit` es true (mayoreo a crédito) NO se cobra; `collect_minor` es lo que hay que cobrar | Device + Staff shift | | GET | `/busy` | «Mucha demanda»: si está activo (a mano o por la cola), minutos extra y umbral automático | Device + Staff shift | | POST | `/busy` | Enciende o apaga «Mucha demanda» y los minutos extra; el umbral automático requiere device.manage. Al encenderlo avisa a los pedidos en curso | Device + Staff shift | | POST | `/orders/{id}/accept` | Acepta el pedido de un cliente (cocina o caja). Sin respuesta en 10 min se cancela solo | Device + Staff shift | | POST | `/orders/{id}/reject` | Rechaza el pedido de un cliente con un motivo: se cancela y se devuelve lo pagado con Fina Pay | Device + Staff shift | | GET | `/deliveries` | Pedidos a domicilio en curso: cliente, dirección, cobro y quién lo lleva. Cada pedido trae `payment` (`collect_minor` = lo que se cobra al entregar; 0 si `on_credit` o ya pagado → «A crédito · no cobrar»). `summary.to_collect_minor` y `cash_collected_minor` excluyen lo a crédito | Device + Staff shift | | POST | `/deliveries/{id}/dispatch` | Salgo con el pedido: pasa a en camino | Device + Staff shift | | POST | `/deliveries/{id}/deliver` | Pedido entregado | Device + Staff shift | | GET | `/cashier/close` | Cierre de caja del día (?date=YYYY-MM-DD): totales por método, por cajero, efectivo esperado y anulados | Device + Staff shift | | GET | `/floor` | Plano con el estado vivo de cada mesa | Device + Staff shift | | GET | `/tables/{id}` | Cuenta de la mesa | Device + Staff shift | | PATCH | `/tables/{id}` | Asigna la mesa a un mesero | Device + Staff shift | | POST | `/tables/{id}/orders` | Toma un pedido para la mesa | Device + Staff shift | | POST | `/tables/{id}/close` | Cobra todo lo pendiente de la mesa y la libera; cuerpo opcional {payment_method, cash_received_minor} | Device + Staff shift | | GET | `/menu` | La carta (también con menu.availability). Cada plato trae `model_3d` (modelo 3D publicado de «Platos en AR») o null | Device + Staff shift | | POST | `/menu/{dishId}/availability` | Agota o devuelve un plato (86), o una opción o extra suyo con value_id | Device + Staff shift | | GET | `/calls` | Llamadas de mesa abiertas y atendidas | Device + Staff shift | | POST | `/calls/{id}/ack` | Atiende una llamada | Device + Staff shift | | POST | `/calls/{id}/resolve` | Resuelve una llamada | Device + Staff shift | | GET | `/reservations` | Reservas del día (?date=YYYY-MM-DD) | Device + Staff shift | | POST | `/reservations/{id}/status` | Confirma, sienta o cancela una reserva | Device + Staff shift | | GET | `/dealer/leads` | Concesionario, «mis prospectos» (?status=open\|all): los asignados al vendedor ligado a este empleado; con dealer.leads.view_all, los de todo el equipo. Sin vendedor ligado devuelve linked:false y lista vacía | Device + Staff shift | | GET | `/dealer/test-drives` | Concesionario, «mis pruebas de manejo» (?date=YYYY-MM-DD&days=1..7): las del vendedor ligado a este empleado; con dealer.test_drives.view_all, las de todo el equipo | Device + Staff shift | | GET | `/farm/orders` | Granja, pedidos (?filter=pending\|today\|upcoming\|active\|done) con el contador de cada pestaña: {orders, counts}. Cada pedido trae `on_credit` y `payment` (`OrderPayment`) | Device + Staff shift | | GET | `/farm/today` | Granja, «Hoy»: foto del día en una respuesta (?low_stock_threshold=1..1000, 5 por defecto): {date, generated_at, now_min, currency_code, counts{pending,today,active,delivered_today,cancelled_today}, sold_today, next_window\|null, alerts, stock\|null (solo con farm.stock.manage), couriers} | Device + Staff shift | | GET | `/farm/orders/{id}` | Granja, detalle de un pedido: artículos, línea de tiempo (by/actor_name/note), avisos y nota del cliente | Device + Staff shift | | POST | `/farm/orders/{id}/approve` | Granja, aprueba un pedido que espera aceptación (409 ORDER_ALREADY_APPROVED / ORDER_ALREADY_CANCELLED) | Device + Staff shift | | POST | `/farm/orders/{id}/reject` | Granja, rechaza un pedido que espera aceptación; cuerpo opcional {reason}. Se devuelve lo pagado | Device + Staff shift | | POST | `/farm/orders/{id}/status` | Granja, mueve el estado {status: preparing\|ready_for_pickup\|on_the_way\|delivered, courier_id?}; solo hacia adelante (409 INVALID_TRANSITION / ORDER_NOT_APPROVED) | Device + Staff shift | | GET | `/farm/couriers` | Granja, repartidores activos para «¿Quién lo lleva?» | Device + Staff shift | | GET | `/farm/packing` | Granja, lista de empaque del día (?date=YYYY-MM-DD) por franja: productos sumados, pedidos y marcas de empacado | Device + Staff shift | | POST | `/farm/packing/marks` | Granja, marca empacada una línea {kind:"line", date, window_key, line_key} o un pedido {kind:"order", order_id} | Device + Staff shift | | DELETE | `/farm/packing/marks/{id}` | Granja, quita una marca de empacado | Device + Staff shift | | POST | `/farm/orders/{id}/cancel` | Granja, cancela un pedido {reason}: avisa al cliente y devuelve el stock | Device + Staff shift | | POST | `/farm/orders/{id}/alerts` | Granja, avisa al cliente de un pedido {reason, message?} (push order.alert) | Device + Staff shift | | POST | `/farm/orders/{id}/alerts/{alertId}/resolve` | Granja, marca resuelto un aviso de pedido | Device + Staff shift | | GET | `/farm/notice` | Granja, aviso general vigente y cuántos clientes tienen la app: {notice\|null, app_customers} | Device + Staff shift | | PUT | `/farm/notice` | Granja, publica o reemplaza el aviso general {level, title, message, until?, send_push} | Device + Staff shift | | DELETE | `/farm/notice` | Granja, quita el aviso general | Device + Staff shift | | GET | `/farm/products` | Granja, productos activos con stock, disponibilidad y variantes | Device + Staff shift | | PATCH | `/farm/products/{id}` | Granja, agota, repone o ajusta el stock {available?, stock?, variant_id?} (disponible = stock mayor a 0) | Device + Staff shift | | GET | `/appointments` | Citas, agenda del día (?date=YYYY-MM-DD&professional_id=\|unassigned): con appointments.view las del profesional ligado a este empleado y las sin asignar; con appointments.view_all (que también abre la agenda), las de todo el equipo. Sin profesional ligado devuelve linked:false y lista vacía | Device + Staff shift | | GET | `/appointments/pending` | Citas, solicitudes por aprobar desde hoy: {count, appointments} | Device + Staff shift | | GET | `/appointments/{id}` | Citas, detalle con nota del cliente, nota interna, respuestas del formulario, precio y las acciones permitidas (actions). Fuera del alcance responde 404 | Device + Staff shift | | POST | `/appointments/{id}/approve` | Citas, aprueba una solicitud (avisa al cliente). 409 APPOINTMENT_NOT_PENDING / APPOINTMENT_UNPAID | Device + Staff shift | | POST | `/appointments/{id}/reject` | Citas, rechaza una solicitud {reason 3-200}: se cancela y se avisa al cliente. 409 APPOINTMENT_NOT_PENDING | Device + Staff shift | | POST | `/appointments/{id}/complete` | Citas, marca completada una cita confirmada (desde 15 min antes de su inicio; no crea ingreso). 409 APPOINTMENT_NOT_STARTED / APPOINTMENT_STATE_CONFLICT | Device + Staff shift | | POST | `/appointments/{id}/no-show` | Citas, marca «no asistió» una cita confirmada ya empezada (estado derivado no_show; no avisa al cliente). 409 APPOINTMENT_NOT_STARTED / APPOINTMENT_STATE_CONFLICT | Device + Staff shift | | DELETE | `/appointments/{id}/no-show` | Citas, deshace «no asistió» el mismo día local del inicio o el siguiente. 409 APPOINTMENT_STATE_CONFLICT | Device + Staff shift | | GET | `/appointments/{id}/slots` | Citas, huecos libres para reprogramar (?date=YYYY-MM-DD): {date, duration_min, professional_id, starts, reason: null\|closed\|service_unavailable} | Device + Staff shift | | POST | `/appointments/{id}/reschedule` | Citas, reprograma a un hueco libre {date, start_min} (conserva duración y profesional; avisa al cliente). 409 SLOT_NOT_AVAILABLE / APPOINTMENT_STATE_CONFLICT | Device + Staff shift | | POST | `/appointments/{id}/cancel` | Citas, cancela una cita {reason 3-200}: avisa al cliente y revierte el ingreso registrado si el negocio devuelve en cancelaciones. 409 APPOINTMENT_STATE_CONFLICT | Device + Staff shift | | PUT | `/appointments/{id}/staff-note` | Citas, nota interna del equipo {note: ≤1000 \| null} (no la ve el cliente) | Device + Staff shift | | GET | `/dealer/today` | Concesionario, «Hoy» del vendedor: {date, generated_at, linked, leads\|null, test_drives\|null}; exige algún permiso de ver (leads o pruebas), contadores del alcance, siguiente prueba y hasta 5 prospectos en «focus» | Device + Staff shift | | GET | `/dealer/leads/{id}` | Concesionario, detalle de un prospecto del alcance: línea de tiempo con quién hizo cada cosa, seguimiento pendiente, etapas permitidas, motivos de pérdida y `actions` (404 fuera del alcance) | Device + Staff shift | | POST | `/dealer/leads/{id}/stage` | Concesionario, mueve la etapa {stage: contacted\|test_drive\|negotiation\|financing\|lost, note?, lost_reason?, lost_note?} (nunca won). 409 LEAD_STAGE_CONFLICT; 400 INVALID_REQUEST con details.reason | Device + Staff shift | | POST | `/dealer/leads/{id}/activities` | Concesionario, registra llamada, WhatsApp, nota, visita o correo {kind, body?, follow_up_at?}: {activity_id, first_contact} | Device + Staff shift | | POST | `/dealer/leads/{id}/follow-ups/{activityId}/complete` | Concesionario, cierra un seguimiento pendiente del prospecto: {done} | Device + Staff shift | | GET | `/dealer/test-drives/{id}` | Concesionario, detalle de una prueba de manejo del alcance con hitos y `actions` (404 fuera del alcance) | Device + Staff shift | | POST | `/dealer/test-drives/{id}/confirm` | Concesionario, confirma una prueba solicitada y avisa al cliente. 409 TEST_DRIVE_STATE_CONFLICT | Device + Staff shift | | POST | `/dealer/test-drives/{id}/check-in` | Concesionario, check-in {license_verified: true, odometer_out: 0..3000000}. 400 ODOMETER_INVALID / INVALID_REQUEST (LICENSE_NOT_VERIFIED); 409 TEST_DRIVE_STATE_CONFLICT | Device + Staff shift | | POST | `/dealer/test-drives/{id}/close` | Concesionario, cierra la prueba {odometer_in, outcome: liked\|undecided\|not_interested, feedback?}. 400 ODOMETER_INVALID (ODOMETER_LOWER); 409 TEST_DRIVE_STATE_CONFLICT | Device + Staff shift | | POST | `/dealer/test-drives/{id}/no-show` | Concesionario, marca «no asistió» una prueba cuya hora ya pasó y programa un seguimiento: {status, follow_up_at}. 409 TEST_DRIVE_STATE_CONFLICT | Device + Staff shift | | POST | `/dealer/test-drives/{id}/cancel` | Concesionario, cancela la prueba {reason 3-200} y avisa al cliente. 409 TEST_DRIVE_STATE_CONFLICT | Device + Staff shift | | GET | `/store/today` | Tienda, la foto del día: contadores (pending, to_pack, ready, shipped, delivered_today, cancelled_today), vendido hoy, el más antiguo por aprobar y stock bajo (null sin store.stock.manage). ?low_stock_threshold=1..1000 | Device + Staff shift | | GET | `/store/orders` | Tienda, pedidos por pestaña (?filter=pending\|to_pack\|ready\|shipped\|done) con el contador de cada una | Device + Staff shift | | GET | `/store/orders/{id}` | Tienda, detalle de un pedido: artículos con foto, talla/color y SKU, historial con quién hizo cada cambio, guía de envío y las acciones permitidas (actions) | Device + Staff shift | | POST | `/store/orders/{id}/approve` | Tienda, aprueba un pedido por aprobar (registra el ingreso y descuenta el stock, igual que el panel). 409 ORDER_ALREADY_APPROVED / ORDER_ALREADY_CANCELLED / NO_INCOME_ACCOUNT | Device + Staff shift | | POST | `/store/orders/{id}/reject` | Tienda, rechaza un pedido por aprobar {reason 3-200}: se cancela y se avisa al cliente. 409 ORDER_ALREADY_APPROVED / ORDER_ALREADY_CANCELLED / ORDER_PAID (pagado en línea: se rechaza desde el panel para devolver el dinero) | Device + Staff shift | | POST | `/store/orders/{id}/status` | Tienda, mueve la entrega {status: preparing\|ready_for_pickup\|on_the_way\|delivered, carrier?, tracking_code?} (guía solo al enviar). 409 ORDER_NOT_APPROVED / INVALID_TRANSITION | Device + Staff shift | | GET | `/store/products` | Tienda, productos activos con stock y variantes con sus atributos (?filter=all\|low\|out&q=&low_stock_threshold=) | Device + Staff shift | | PATCH | `/store/products/{id}` | Tienda, agota, repone o ajusta el stock {available?, stock?, variant_id?} (disponible = stock mayor a 0) | Device + Staff shift | | GET | `/realestate/visits` | Inmobiliaria, visitas por bandeja (?tab=new\|today\|upcoming\|past; sin tab, «new» si hay nuevas): {tab, scope, is_agent, counts, unassigned_count, visits}. Con realestate.visits.view las del agente (employee) y las sin asignar si es agente público; con realestate.visits.view_all, todas | Device + Staff shift | | GET | `/realestate/visits/{id}` | Inmobiliaria, detalle de una visita: propiedad, cliente, mensaje, nota interna, sellos, hora sugerida (suggested) y acciones permitidas (actions). Fuera del alcance responde 404 | Device + Staff shift | | POST | `/realestate/visits/{id}/schedule` | Inmobiliaria, confirma con hora o cambia la hora {date: YYYY-MM-DD, time_min: 0-1439 múltiplo de 15, no en el pasado, hasta hoy + 90}. 409 VISIT_STATE_CONFLICT | Device + Staff shift | | POST | `/realestate/visits/{id}/done` | Inmobiliaria, marca realizada una visita confirmada (desde 30 min antes) con resultado opcional {outcome: interested\|not_interested\|wants_another_visit\|made_offer\|null}. 409 VISIT_NOT_STARTED / VISIT_STATE_CONFLICT | Device + Staff shift | | POST | `/realestate/visits/{id}/no-show` | Inmobiliaria, marca «no asistió» una visita confirmada ya empezada (estado derivado no_show; no avisa al cliente). 409 VISIT_NOT_STARTED / VISIT_STATE_CONFLICT | Device + Staff shift | | DELETE | `/realestate/visits/{id}/no-show` | Inmobiliaria, deshace «no asistió» el mismo día local de la visita o el siguiente. 409 VISIT_STATE_CONFLICT | Device + Staff shift | | POST | `/realestate/visits/{id}/cancel` | Inmobiliaria, cancela una visita pendiente o confirmada {reason 3-200}. 409 VISIT_STATE_CONFLICT | Device + Staff shift | | PUT | `/realestate/visits/{id}/note` | Inmobiliaria, nota interna del equipo {note: ≤1000 \| null} (no la ve el cliente) | Device + Staff shift | | POST | `/realestate/visits/{id}/contact` | Inmobiliaria, registra un contacto con el cliente {channel: call\|whatsapp\|email}: {contacted_at, last_contact_at, contact_count}. 409 VISIT_STATE_CONFLICT | Device + Staff shift | | POST | `/realestate/visits/{id}/claim` | Inmobiliaria, un agente público toma una visita sin asignar. 409 VISIT_ALREADY_ASSIGNED / AGENT_NOT_AVAILABLE | Device + Staff shift | | POST | `/realestate/visits/{id}/assign` | Inmobiliaria, asigna o quita el agente {agent_id: uuid \| null}; exige además realestate.visits.view_all (403 FORBIDDEN). Avisa al agente nuevo. 409 AGENT_NOT_AVAILABLE / VISIT_STATE_CONFLICT | Device + Staff shift | | GET | `/realestate/agents` | Inmobiliaria, agentes públicos activos para «Asignar agente»: {agents: [{id, name, photo_url, open_visits, is_me}]} | Device + Staff shift | | GET | `/realestate/properties` | Inmobiliaria, propiedades en alcance (?status=available\|reserved\|all, «available» por defecto): {scope, counts{available,reserved,other}, properties} con visitas pendientes y próximas | Device + Staff shift | | PATCH | `/realestate/properties/{id}` | Inmobiliaria, cambia una propiedad entre disponible y reservada {status: available\|reserved}. 409 PROPERTY_STATE_CONFLICT (desde alquilada, vendida o inactiva) | Device + Staff shift | | GET | `/rentcar/today` | Rent Car, «Hoy»: {date, generated_at, counts{pending,pickups,returns,not_returned,active}, fleet{available,rented,in_shop,inactive}, pickups, returns}. 404 NOT_FOUND si el negocio no tiene el módulo | Device + Staff shift | | GET | `/rentcar/reservations` | Rent Car, reservas de vehículos (?tab=pending\|pickups\|returns\|active\|upcoming; sin tab, la primera con reservas): {tab, counts, reservations} | Device + Staff shift | | GET | `/rentcar/reservations/{id}` | Rent Car, detalle: reserva, nota, servicios, inspecciones (URLs firmadas, inspected_by_name), comparison salida/llegada, pickup_reference y las acciones permitidas (actions) | Device + Staff shift | | POST | `/rentcar/reservations/{id}/approve` | Rent Car, aprueba una reserva pendiente (avisa al cliente). 409 RESERVATION_STATE_CONFLICT / RESERVATION_AWAITING_PAYMENT (se confirma sola al pagar) | Device + Staff shift | | POST | `/rentcar/reservations/{id}/reject` | Rent Car, rechaza una reserva pendiente {reason 3-200}: se cancela, se cancela el cobro pendiente y se avisa al cliente. 409 RESERVATION_STATE_CONFLICT | Device + Staff shift | | POST | `/rentcar/reservations/{id}/photos` | Rent Car, sube una foto de inspección (multipart, campo file; 10 MB): {id, url}. 413 FILE_TOO_LARGE / 415 UNSUPPORTED_IMAGE / 429 UPLOAD_LIMIT / 503 STORAGE_UNAVAILABLE | Device + Staff shift | | DELETE | `/rentcar/reservations/{id}/photos/{photoId}` | Rent Car, borra una foto que aún no está en una inspección guardada | Device + Staff shift | | POST | `/rentcar/reservations/{id}/checkout` | Rent Car, check-out {odometer_km, fuel_eighths 0-8, notes?, photos[1-24]{id, angle, damage_note?}}: guarda la inspección de salida con el nombre del empleado y pasa a en curso. 409 RESERVATION_NOT_STARTED / VEHICLE_IN_SHOP / RESERVATION_STATE_CONFLICT; 400 INSPECTION_NEEDS_PHOTOS / INVALID_PHOTO | Device + Staff shift | | POST | `/rentcar/reservations/{id}/checkin` | Rent Car, check-in (mismo cuerpo): guarda la inspección de llegada, completa la reserva y devuelve {status, inspection, comparison}. También recibe una completada por el cron (hasta 3 días). 409 INSPECTION_REQUIRED; 400 ODOMETER_BELOW_PICKUP {pickup_km} / INSPECTION_NEEDS_PHOTOS / INVALID_PHOTO | Device + Staff shift | | GET | `/rentcar/fleet` | Rent Car, flota (?state=all\|available\|rented\|in_shop): {counts, vehicles} con estado derivado, alquiler en curso, próxima reserva y reservas por venir | Device + Staff shift | | POST | `/rentcar/fleet/{id}/shop` | Rent Car, manda un vehículo al taller {note?≤200}: deja de verse en la app de clientes; las reservas no cambian. {state, upcoming_count}. 409 VEHICLE_RENTED / VEHICLE_IN_SHOP | Device + Staff shift | | DELETE | `/rentcar/fleet/{id}/shop` | Rent Car, saca un vehículo del taller: {state: available\|rented}. 409 VEHICLE_NOT_IN_SHOP | Device + Staff shift | | GET | `/loyalty` | Fidelidad, el programa resumido: {program: {name, status, points_label_singular, points_label_plural, currency_code, enrollment_mode, starts_at, ends_at, rules{per_purchase, per_visit, per_amount, min_purchase_minor}, self_check_in}\|null} | Device + Staff shift | | GET | `/loyalty/members` | Fidelidad, busca socios (?q= nombre o teléfono&cursor=): {members, next_cursor}. Cada socio trae nombre, teléfono enmascarado («•••• 1234»; nunca el correo), saldo, estado y última actividad | Device + Staff shift | | POST | `/loyalty/members` | Fidelidad, inscribe un cliente {client_id} o crea el cliente {name, phone} y lo inscribe: {created, client_created, member}. 403 LOYALTY_MEMBER_INACTIVE si está bloqueado | Device + Staff shift | | GET | `/loyalty/members/by-card/{token}` | Fidelidad, el socio de una tarjeta escaneada; {token} es el token o la URL completa del QR (https://…/f/, codificada). 404 LOYALTY_NOT_FOUND | Device + Staff shift | | GET | `/loyalty/members/{id}` | Fidelidad, detalle del socio: saldo, canjes pendientes (pending_redemptions) y últimos 20 movimientos (movements) | Device + Staff shift | | POST | `/loyalty/members/{id}/earn` | Fidelidad, acredita {kind: visit\|purchase\|visit_purchase, amount_minor?, store_order_id?, invoice_id?}. Idempotency-Key OBLIGATORIO (400 IDEMPOTENCY_KEY_REQUIRED). Con store_order_id (aprobado) o invoice_id (pagada) el origen es ese documento, del MISMO negocio y del cliente del socio (el pedido de sala sin cliente se permite asociar), y el IMPORTE lo toma el servidor (amount_minor se ignora); no da puntos dos veces. Sin documento, staff_purchase con amount_minor acotado a 5000000 (400 INVALID_REQUEST si lo supera) y siempre sujeto al tope diario de puntos. {status: awarded\|candidate\|duplicate, points, balance}. 409 LOYALTY_PROGRAM_INACTIVE / ORDER_STATE_CONFLICT / ORDER_CLIENT_MISMATCH / INVOICE_STATE_CONFLICT / INVOICE_CLIENT_MISMATCH | Device + Staff shift | | POST | `/loyalty/members/{id}/adjust` | Fidelidad, ajusta puntos a mano {delta ≠ 0, reason 3-200}. Idempotency-Key OBLIGATORIO. 409 LOYALTY_INSUFFICIENT_POINTS si dejaría el saldo bajo 0 | Device + Staff shift | | GET | `/loyalty/redemptions` | Fidelidad, canjes del negocio (?status=pending\|used\|expired\|cancelled, pending por defecto), los 50 más recientes | Device + Staff shift | | POST | `/loyalty/redemptions/validate` | Fidelidad, mira un código de canje SIN marcarlo {code}: {valid, error_code, redemption\|null} | Device + Staff shift | | POST | `/loyalty/redemptions/{id}/use` | Fidelidad, entrega la recompensa (marca usado). 409 LOYALTY_CODE_ALREADY_USED / LOYALTY_CODE_CANCELLED, 410 LOYALTY_CODE_EXPIRED | Device + Staff shift | | POST | `/loyalty/redemptions/{id}/cancel` | Fidelidad, cancela un canje y devuelve los puntos {reason 3-200?}. Un canje ya usado solo lo cancela quien tiene loyalty.adjust y con motivo (403 LOYALTY_NOT_AVAILABLE si no; el permiso base se acepta también con solo loyalty.adjust) | Device + Staff shift | | GET | `/advisory/cases` | Asesoría, mis casos (?status=open\|all\|requested\|active\|on_hold\|completed\|cancelled\|declined, open por defecto &stage=&q=&mine=1): {cases}. Sin advisory.view_all solo los que lidero o donde tengo una visita asignada; con view_all, los de todo el equipo (mine=1 los acota a los míos). También lo abre view_all | Device + Staff shift | | POST | `/advisory/cases` | Asesoría, crea un caso activo {client_id \| client_name+client_phone, case_type_id, title, summary?, owner_employee_id?, priority?, stage_key?, target_date?, site_name?, site_address?, site_lat?, site_lng?}: {case, client_created}. Idempotency-Key OBLIGATORIO (reenviar devuelve el mismo caso) | Device + Staff shift | | GET | `/advisory/case-types` | Asesoría, tipos de caso vigentes con sus etapas, para crear un caso: {case_types} | Device + Staff shift | | GET | `/advisory/employees` | Asesoría, el equipo para «Asignar a…» (responsable o técnico de una visita): {employees} | Device + Staff shift | | GET | `/advisory/cases/{id}` | Asesoría, detalle del caso: {case, activities (las 50 últimas, todas las visibilidades)}. El teléfono del cliente va enmascarado («•••• 1234») y sin correo salvo advisory.view_all; los contadores de mercado solo cuentan con advisory.market. También lo abre view_all o advisory.visits. 404 ADVISORY_NOT_FOUND si no es de mi alcance | Device + Staff shift | | GET | `/advisory/cases/{id}/contact` | Asesoría, el teléfono real para llamar o escribir por WhatsApp {name, phone, tel, whatsapp}. Registra el acceso en la actividad del staff. También lo abre view_all o advisory.visits | Device + Staff shift | | POST | `/advisory/cases/{id}/activities` | Asesoría, anota en la bitácora {kind: note\|call\|whatsapp\|email\|meeting\|task, body ≤4000, visibility: internal\|client, due_at? (obligatorio en task)}: {activity}. Idempotency-Key OBLIGATORIO. 409 ADVISORY_CASE_CLOSED si el caso está cerrado y no es una nota interna | Device + Staff shift | | POST | `/advisory/cases/{id}/activities/{aid}/done` | Asesoría, marca hecha una tarea (idempotente): {activity} | Device + Staff shift | | POST | `/advisory/cases/{id}/stage` | Asesoría, cambia de etapa {stage_key, tell_client?}: {case, stage_key}. 409 ADVISORY_INVALID_TRANSITION si el caso no está abierto o ya está en esa etapa | Device + Staff shift | | POST | `/advisory/cases/{id}/status` | Asesoría, cambia el estado {action: accept\|decline\|hold\|resume\|complete\|cancel\|reopen, outcome? (complete), reason? (decline\|cancel), owner_employee_id?, stage_key? (accept)}: {case}. 409 ADVISORY_INVALID_TRANSITION / 422 ADVISORY_FIELDS_MISSING | Device + Staff shift | | POST | `/advisory/cases/{id}/owner` | Asesoría, cambia o quita el responsable {employee_id: uuid \| null}: {case}. Exige además advisory.view_all (403 ADVISORY_FORBIDDEN) | Device + Staff shift | | GET | `/advisory/cases/{id}/dossiers` | Asesoría, expedientes del caso con ítems (estado, motivo de rechazo, vencimiento) y archivos (sin rutas de almacenamiento): {dossiers}. También lo abre view_all | Device + Staff shift | | POST | `/advisory/dossier-items/{id}/review` | Asesoría, revisa un documento {action: approve\|reject\|waive\|unwaive\|reopen, reason? (obligatorio en reject\|waive\|reopen), expires_on? (obligatorio al aprobar un ítem que vence)}: {item}. 409 ADVISORY_ITEM_LOCKED / ADVISORY_INVALID_TRANSITION | Device + Staff shift | | POST | `/advisory/dossier-items/{id}/files` | Asesoría, adjunta una subida a un documento {upload_id} (de POST /advisory/uploads): {item}. 415 ADVISORY_FILE_TYPE si el ítem no acepta ese tipo | Device + Staff shift | | GET | `/advisory/files/{id}/url` | Asesoría, URL firmada y corta de un archivo del expediente {url, expires_in}; el acceso se registra antes de firmar. Las fotos de visita también las abre advisory.visits. 404 ADVISORY_NOT_FOUND si no es de mi alcance | Device + Staff shift | | POST | `/advisory/uploads` | Asesoría, sube un archivo (bytes en el cuerpo con Content-Type y X-File-Name; ?purpose=dossier\|visit_photo, dossier por defecto: visit_photo exige advisory.visits): 201 {upload_id, mime, size_bytes}. Mismo validador que el cliente: 413 ADVISORY_FILE_TOO_LARGE (15 MB), 415 ADVISORY_FILE_TYPE (jpeg/png/webp/heic/pdf, por contenido real), 429 ADVISORY_UPLOAD_LIMIT | Device + Staff shift | | GET | `/advisory/visits` | Asesoría, la agenda de visitas (?from=YYYY-MM-DD&to=YYYY-MM-DD en hora RD, to inclusive; sin rango trae también las por agendar; &mine=1): {visits}. Sin advisory.view_all solo las mías. También lo abre view_all | Device + Staff shift | | GET | `/advisory/visits/{id}` | Asesoría, la visita para su informe: {visit, case, employee, metrics (indicadores medibles con rango y último valor), photos}. El teléfono va enmascarado; el real por /cases/{id}/contact. También lo abren view_all y view | Device + Staff shift | | POST | `/advisory/cases/{id}/visits` | Asesoría, crea una visita {scheduled_at?, duration_min?, employee_id?, site_address?, purpose?, request_note?}: con fecha nace agendada, sin ella por agendar; sin employee_id la toma quien la crea. Asignar a otra persona exige advisory.assign. Idempotency-Key OBLIGATORIO. También lo abre advisory.manage | Device + Staff shift | | POST | `/advisory/visits/{id}/schedule` | Asesoría, agenda una visita pedida o reprograma una agendada {scheduled_at, duration_min?, employee_id?, site_address?, purpose?}. También lo abre advisory.manage | Device + Staff shift | | POST | `/advisory/visits/{id}/start` | Asesoría, inicia la visita (agendada → en curso). 403 ADVISORY_FORBIDDEN si no es mía y no tengo view_all | Device + Staff shift | | POST | `/advisory/visits/{id}/cancel` | Asesoría, cancela la visita {reason?}. También lo abre advisory.manage | Device + Staff shift | | POST | `/advisory/visits/{id}/no-show` | Asesoría, marca «no estaba» una visita agendada | Device + Staff shift | | PUT | `/advisory/visits/{id}/report` | Asesoría, guarda el borrador del informe {findings ≤8000, recommendations[{id, text, priority: high\|medium\|low, due_on?, status: open\|done\|dropped}], measurements[{case_metric_id, value, note?}], photos[{upload_id, caption?}] (de POST /advisory/uploads?purpose=visit_photo, hasta 40)}: {visit}. Se puede repetir mientras la visita no esté cancelada | Device + Staff shift | | POST | `/advisory/visits/{id}/complete` | Asesoría, completa la visita {report?}; exige hallazgos o al menos una recomendación (422 ADVISORY_FIELDS_MISSING). Las mediciones pasan a ser registros del indicador | Device + Staff shift | | POST | `/advisory/visits/{id}/publish` | Asesoría, publica el informe al cliente: {visit, version, republished, pdf, report_url}. Republicar sube la versión con el mismo enlace | Device + Staff shift | | GET | `/advisory/cases/{id}/metrics` | Asesoría, indicadores del caso con su serie e historial: {metrics}. También lo abren view_all y advisory.visits | Device + Staff shift | | POST | `/advisory/cases/{id}/metrics/{cmId}/records` | Asesoría, registra una medición {value, measured_on, note?} dentro del rango del indicador: 201 {record_id}. Idempotency-Key OBLIGATORIO. También lo abre advisory.visits. 404 si el indicador no es de ese caso | Device + Staff shift | | GET | `/advisory/buyers` | Asesoría, compradores activos (?q=): {buyers}. Teléfono enmascarado y sin correo salvo advisory.view_all | Device + Staff shift | | GET | `/advisory/cases/{id}/opportunities` | Asesoría, oportunidades del caso con su comprador y oferta: {opportunities} | Device + Staff shift | | POST | `/advisory/opportunities/{id}/status` | Asesoría, mueve una oportunidad {status: draft\|shared\|negotiating\|agreed\|won\|lost\|withdrawn, price_minor?, currency_code?, volume?, closed_reason?, next_step?}: {opportunity}. Ganar exige precio y volumen (422 ADVISORY_FIELDS_MISSING) | Device + Staff shift | Source: https://fina.do/developers#api-staff --- ## Video Studio API > Quoted, idempotent AI media jobs paid with your Fina credits. Quoted, idempotent AI media jobs paid with your Fina credits. | Field | Value | | --- | --- | | Base URL | `https://fina.do/api/video-studio/v1` | | Authentication | Bearer personal API key (`fina_vs_…`) | | OpenAPI 3.1 | [/api/video-studio/v1/openapi.json](https://fina.do/api/video-studio/v1/openapi.json) | | Built for | Scripts and tools of a Fina Video Studio user | Create a personal API key in your Fina profile. Every spending mutation needs an `Idempotency-Key` header; credits are reserved at the quoted maximum and never charged above it. #### Endpoints Base URL: `https://fina.do/api/video-studio/v1` | Method | Path | Summary | Auth | | --- | --- | --- | --- | | GET | `/me` | Identity, plan and spendable credits | Personal key | | GET | `/businesses` | Your businesses (not archived), by name | Personal key | | GET | `/businesses/{id}` | Everything needed to make videos and images of one of your businesses | Personal key | | GET | `/locals` | Your locals («Mis locales»: visual profiles of your physical spaces) | Personal key | | GET | `/locals/{id}` | One of your locals: description, usable photos and the linked business | Personal key | | GET | `/catalog` | Server-driven catalog of operations and models | Personal key | | POST | `/quotes` | Create an immutable, expiring quote | Personal key | | POST | `/uploads` | Register an input and get a signed upload URL | Personal key | | POST | `/uploads/{id}/complete` | Verify the uploaded object (existence, size, sha256 when feasible) | Personal key | | GET | `/jobs` | List jobs (cursor pagination) | Personal key | | POST | `/jobs` | Create a job (reserves the quoted maximum before any provider call) | Personal key | | GET | `/jobs/{id}` | Job detail (reconciles with the provider when pending) | Personal key | | POST | `/jobs/{id}/cancel` | Cancel a job (free before submit; honest otherwise) | Personal key | | GET | `/assets/{id}/download` | Short-lived signed download URL for your own asset | Personal key | Source: https://fina.do/developers#api-video-studio --- ## Errors > HTTP status codes and stable error codes of the Fina Pay and storefront APIs. Fina Pay answers errors as `{ "error": "" }`. Storefront APIs use the envelope `{ "data": null, "error": { "code", "message", "details"? }, "request_id" }`. In both cases branch on the code. | HTTP | Fina Pay code | What to do | | --- | --- | --- | | 400 | `invalid_json`, `invalid_amount`, `missing_params` | Fix the request. `amount_minor` must be an integer > 0. | | 401 | `unauthorized` | Check the secret key and that the connection is active. | | 400 | `reference_required`, `invalid_reference`, `same_account`, … | Payout request errors, see [Payouts](https://fina.do/developers/payouts.md). | | 403 | `payouts_disabled` | Turn on Payouts via API for the connection. | | 404 | `not_found` | The charge or payout does not exist or belongs to another connection. | | 404 | `recipient_not_found` | No active Fina Pay wallet with that account number or phone. | | 409 | `insufficient_funds`, `currency_mismatch`, `wallet_blocked` | The payout was not sent; nothing moved. | | 409 | `not_cancellable` | The charge is no longer `pending`. | | 429 | `rate_limited` | Back off and retry (60 requests/minute per connection). | | 5xx | — | Retry with exponential backoff; reuse the same `reference`. | Each storefront spec lists its codes per status in its OpenAPI (`responses`). Common ones: `UNAUTHENTICATED`, `APP_NOT_FOUND`, `BUSINESS_INACTIVE`, `OUT_OF_DELIVERY_ZONE`, `SHIPMENT_LIMIT_EXCEEDED`, `INSUFFICIENT_STOCK`, `RATE_LIMITED`. Source: https://fina.do/developers#errors --- ## Testing and going live > How to test without a sandbox, and a go-live checklist. > **Warning:** There is no separate sandbox environment yet: keys are `live` and charges move real money. Test with small amounts, then cancel or let them expire. 1. Create a dedicated connection for testing (for example "Staging") with its own keys and webhook URL, so test charges are booked and reported apart. 2. Create a charge of `100` (RD$1.00), pay it with a second Fina account and check `charge.paid` arrives and verifies. 3. Expose your local server with a tunnel (ngrok, Cloudflare Tunnel) to receive webhooks during development. 4. Cancel pending test charges with `POST /charges/{id}/cancel`. #### Go-live checklist - Secret key and IPN secret live only in server environment variables. - Webhook handler verifies `x-fina-sig`, answers 2xx within 15 s and is idempotent. - Charges use your order id as `reference`; retries reuse it. - Money is handled as integers in centavos end to end. - You reconcile with `GET /charges/{id}` when a webhook is missing. Source: https://fina.do/developers#testing --- ## Changelog > Notable changes to the public APIs and these docs. | Date | Change | | --- | --- | | 2026-10-08 | Fina Pay payouts: `POST /payouts`, `GET /payouts/{id}` and `GET /recipients` (off by default, enabled per connection). | | 2026-10-05 | New developer portal: OpenAPI spec for Fina Pay (`/api/wallet/v1/openapi.json`), `llms.txt`, `llms-full.txt`, Markdown pages and the read-only docs MCP server. | | 2026-10-05 | Storefront APIs: weight-based shipping in `/config` and `/delivery/estimate` (`items`, `weight`, `fee_by_weight`, `SHIPMENT_LIMIT_EXCEEDED`). | | 2026-10-05 | Fina Pay subscriptions: hosted checkout `/pay/s/`, customer portal and `subscription.*` webhooks (rolling out). | | 2026-10-04 | `charge.refunded` webhook when a paid charge is returned to the customer. | | 2026-10-03 | Fina Pay: `fee_minor` and `net_minor` on paid charges and in `charge.paid`. | | 2026-07-05 | Fina Pay API v1: charges, payment page and signed IPN webhooks. | Source: https://fina.do/developers#changelog ---