Empezar
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 | Secret key sk_live_… |
| Build my own customer app or website for a Fina business | Storefront APIs | App key of the module |
| Build a kitchen display / POS for my restaurant | Restaurant staff API | Paired device + staff PIN |
| Automate AI media jobs with my credits | Video Studio API | 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 indexes them, llms-full.txt has all of it in one file, and a read-only MCP server lets Claude Code, Cursor and other assistants search the docs and read the OpenAPI specs.
Empezar
Quickstart
Create an API key, take your first payment and receive the webhook in about five minutes.
1. Create your API keys
- Activate Fina Pay on your personal account in /app/wallet.
- Open your business panel, edit your business and go to the Connectors tab → Fina Pay.
- Create the connection: pick the income account and category where paid charges are booked.
- Copy the public key (
pk_live_…) and the secret key (sk_live_…). The secret is shown only once — store it asFINA_SECRET_KEYin your server environment. - Optional but recommended: set a webhook URL (HTTPS) and an IPN secret. Store the secret as
FINA_IPN_SECRET.
2. Create your first charge
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" }
}'{
"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.
// 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
})The verifyFinaSignature helper is in Webhooks. No webhook URL yet? Poll GET /charges/{id} until status is paid.
Empezar
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.
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: <describe what you want, e.g. "add a Pay with Fina button to checkout and mark the order paid on charge.paid">Connect the docs MCP server
A read-only Model Context Protocol 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.
claude mcp add --transport http fina-docs https://fina.do/api/developers/mcpLLM-friendly files
| URL | What it is |
|---|---|
| /llms.txt | Index in the llmstxt.org format: one link per page, to its Markdown version. |
| /llms-full.txt | All the documentation in one Markdown file, endpoint tables included. |
/developers/<page>.md | Any single page as Markdown, e.g. /developers/webhooks.md. |
…/openapi.json | The machine-readable contract of each API (see API reference). |
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.
// 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)Fina Pay
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 |
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.
Fina Pay
Charges
Lifecycle of a Fina Pay charge: statuses, expiry, fees, cancellation and refunds.
Lifecycle
- Your backend calls
POST /charges. The charge ispendingand has apayment_url. - The customer opens
payment_url, signs in to Fina and confirms with their wallet PIN. - The charge becomes
paid; you receivecharge.paidandGET /charges/{id}showspaid_at,fee_minorandnet_minor. - Unpaid charges become
expiredatexpires_at(24 h by default,expires_in_secondsto change it), orcancelledif you callPOST /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
curl https://fina.do/api/wallet/v1/charges/CHARGE_ID \
-H "Authorization: Bearer $FINA_SECRET_KEY"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.
Fina Pay
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.
Flow
- Your user enters their Fina Pay account number (10 digits) or the phone linked to their wallet.
- Call
GET /recipients?account=…and show them thenamethe money will go to, so they can confirm it is them. - Call
POST /payoutswith your withdrawal id asreference. A201means the money is already in their wallet; they get a notification. - Store the payout
id.GET /payouts/{id}returns it again at any time.
curl "https://fina.do/api/wallet/v1/recipients?account=8095551234" \
-H "Authorization: Bearer $FINA_SECRET_KEY"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"
}'| 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.
Fina Pay
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.paidcharge.expiredcharge.cancelledcharge.refunded
{
"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.
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))
}Delivery and retries
- Answer any
2xxwithin 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, orevent_idfor subscriptions) and, when in doubt, read the current state withGET /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.
Fina Pay
Subscriptions
Recurring Fina Pay payments: hosted checkout, customer portal and lifecycle webhooks (rolling out).
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/<token>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
entitledandentitled_until: give access whileentitledis true. Grace periods and trials are already applied.
{
"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"
}period, charge) contribute only keys that also exist at the top level. Use the helpers in Webhooks as-is — they reproduce this exactly — and re-read sensitive nested values from Fina when they drive money decisions.Apps
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 | /api/farm/v1 | X-Farm-App-Key |
| Boutique storefront API | /api/boutique/v1 | X-Boutique-App-Key |
| Restaurant storefront API | /api/restaurant/v1 | X-Restaurant-App-Key |
| Barbershop booking API | /api/barbershop/v1 | X-Barbershop-App-Key |
| Real estate API | /api/real-estate/v1 | X-Real-Estate-App-Key |
| Rent a car API | /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 needAuthorization: Bearer <access token>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.
curl https://fina.do/api/boutique/v1/config \
-H "X-Boutique-App-Key: $APP_KEY"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.
Referencia de la API
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.
| 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 |
| Built for | Merchants and developers integrating payments |
Webhooks are part of the spec too (webhooks in OpenAPI 3.1). Guides: Quickstart, Charges, Webhooks.
Endpoints
https://fina.do/api/wallet/v1- GET
/charges/{id}Secret keyGet a charge (poll its status, fee and net)
- GET
/payouts/{id}Secret keyGet a payout of this connection
Referencia de la API
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.
| 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 |
| Built for | Custom customer apps and websites for a farm |
Shared rules (app key, envelope, /config, delivery quotes): Storefront APIs.
Endpoints
https://fina.do/api/farm/v1- GET
/configApp keyIdentidad de la granja y lo que la app necesita para arrancar
- GET
/locationsApp keySucursales activas donde se puede recoger
- GET
/catalogApp keyCatálogo con unidad de venta y precio por pieza derivado
- GET
/plansApp keyPlanes con su precio, su ahorro por ciclo y por pieza
- GET
/windowsApp keyFranjas de una fecha para una modalidad
- PATCH
/meApp key + Customer tokenEditar nombre y teléfono
- GET
/addressesApp key + Customer tokenLibreta de direcciones
- POST
/addressesApp key + Customer tokenGuardar una dirección
- PATCH
/addresses/{id}App key + Customer tokenEditar una dirección
- GET
/ordersApp key + Customer tokenPedidos del cliente
- GET
/orders/{id}App key + Customer tokenDetalle y seguimiento de un pedido
- GET
/trackingApp key + Customer tokenPedidos en curso
- GET
/subscriptionsApp key + Customer tokenPlanes del cliente
- GET
/subscriptions/{id}App key + Customer tokenDetalle con sus ciclos
- DELETE
/subscriptions/{id}App key + Customer tokenCancelar el plan
- POST
/subscriptions/{id}/pauseApp key + Customer tokenPausar
- POST
/subscriptions/{id}/resumeApp key + Customer tokenReanudar
- GET
/advisory/cases/{id}App key + Customer tokenCase detail
- GET
/advisory/cases/{id}/dossiersApp key + Customer tokenDocument folders of the case
- GET
/advisory/cases/{id}/visitsApp key + Customer tokenVisits of the case
- POST
/advisory/visits/{id}/acknowledgeApp key + Customer tokenAcknowledge a published visit report
- POST
/devicesApp key + Customer tokenRegistrar el token push del dispositivo
- DELETE
/devices/{id}App key + Customer tokenRevocar el token
Referencia de la API
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.
| 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 |
| Built for | Custom customer apps and websites for a store |
Shared rules (app key, envelope, /config, delivery quotes): Storefront APIs.
Endpoints
https://fina.do/api/boutique/v1- GET
/locationsApp keySucursales activas donde se puede recoger
- GET
/windowsApp keyFranjas de una fecha para una modalidad
- PATCH
/meApp key + Customer tokenEditar nombre y teléfono
- GET
/addressesApp key + Customer tokenLibreta de direcciones
- POST
/addressesApp key + Customer tokenGuardar una dirección
- PATCH
/addresses/{id}App key + Customer tokenEditar una dirección
- GET
/ordersApp key + Customer tokenPedidos del cliente
- GET
/orders/{id}App key + Customer tokenDetalle y seguimiento de un pedido
- GET
/trackingApp key + Customer tokenPedidos en curso
- GET
/advisory/cases/{id}App key + Customer tokenCase detail
- GET
/advisory/cases/{id}/dossiersApp key + Customer tokenDocument folders of the case
- GET
/advisory/cases/{id}/visitsApp key + Customer tokenVisits of the case
- POST
/advisory/visits/{id}/acknowledgeApp key + Customer tokenAcknowledge a published visit report
- POST
/devicesApp key + Customer tokenRegistrar el token push del dispositivo
- DELETE
/devices/{id}App key + Customer tokenRevocar el token
Referencia de la API
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.
| 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 |
| Built for | Custom customer apps and websites for a restaurant |
Shared rules (app key, envelope, /config, delivery quotes): Storefront APIs.
Endpoints
https://fina.do/api/restaurant/v1- GET
/locationsApp keySucursales activas donde se puede recoger
- GET
/catalogApp keyLa carta: categorías del menú y platos con sus tamaños, extras y alérgenos
- GET
/windowsApp keyFranjas de una fecha para una modalidad
- PATCH
/meApp key + Customer tokenEditar nombre y teléfono
- GET
/addressesApp key + Customer tokenLibreta de direcciones
- POST
/addressesApp key + Customer tokenGuardar una dirección
- PATCH
/addresses/{id}App key + Customer tokenEditar una dirección
- GET
/ordersApp key + Customer tokenPedidos del cliente
- GET
/trackingApp key + Customer tokenPedidos en curso
- GET
/subscriptionsApp key + Customer tokenPlanes del cliente
- GET
/subscriptions/{id}App key + Customer tokenUn plan del cliente con sus créditos del ciclo y sus ciclos
- POST
/subscriptions/{id}/pauseApp key + Customer tokenPausar (los créditos ya dados siguen valiendo hasta que caduquen)
- GET
/advisory/cases/{id}App key + Customer tokenCase detail
- GET
/advisory/cases/{id}/dossiersApp key + Customer tokenDocument folders of the case
- GET
/advisory/cases/{id}/visitsApp key + Customer tokenVisits of the case
- POST
/advisory/visits/{id}/acknowledgeApp key + Customer tokenAcknowledge a published visit report
- POST
/devicesApp key + Customer tokenRegistrar el token push del dispositivo
- DELETE
/devices/{id}App key + Customer tokenRevocar el token
- GET
/tables/{token}/callsApp keyCómo van las llamadas de la mesa (abierta o atendida)
- GET
/reservationsApp keyMis reservas (requiere sesión)
- POST
/reservations/{id}/cancelApp keyCancelar una reserva propia que aún no empezó
Referencia de la API
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.
| 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 |
| Built for | Custom booking apps for a barbershop or salon |
Shared rules (app key, envelope, /config, delivery quotes): Storefront APIs.
Endpoints
https://fina.do/api/barbershop/v1- GET
/slotsApp keyCalculate currently available appointment starts
- GET
/meApp key + Customer tokenGet the linked profile for this business
- GET
/appointmentsApp key + Customer tokenList appointments belonging to the linked customer
- GET
/appointments/{id}App key + Customer tokenGet one appointment belonging to the linked customer
- GET
/appointments/{id}/rescheduleApp key + Customer tokenGet valid replacement starts for an appointment
- POST
/appointments/{id}/reviewApp key + Customer tokenReview a completed appointment
- GET
/waitlistApp key + Customer tokenList the linked customer waitlist entries
- DELETE
/waitlist/{id}App key + Customer tokenCancel one waiting entry belonging to the linked customer
- GET
/standingApp key + Customer tokenList the customer's standing appointments
- GET
/advisory/cases/{id}App key + Customer tokenCase detail
- GET
/advisory/cases/{id}/dossiersApp key + Customer tokenDocument folders of the case
- GET
/advisory/cases/{id}/visitsApp key + Customer tokenVisits of the case
- POST
/advisory/visits/{id}/acknowledgeApp key + Customer tokenAcknowledge a published visit report
- POST
/devicesApp key + Customer tokenRegister this installation and its current push token
- DELETE
/devicesApp key + Customer tokenRevoke this app installation for the signed-in account
Referencia de la API
Real estate API
Published properties with filters and visit requests.
Published properties with filters and visit requests.
| 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 |
| Built for | Property listing apps and websites |
Shared rules (app key, envelope, /config, delivery quotes): Storefront APIs.
Endpoints
https://fina.do/api/real-estate/v1- GET
/configApp keyGet 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)
- GET
/catalogApp keyList 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)
- GET
/properties/{id}App keyGet 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)
- GET
/agentsApp keyList the visible agents of the business, ordered by name: { agents: [{ id, name, photo_url, title, languages, properties_count }] }
- GET
/agents/{id}App keyGet one visible agent (Agent schema). A missing, non-public, inactive or foreign agent answers 404 AGENT_NOT_FOUND
- GET
/visit-requestsCustomer tokenList the signed-in customer visit requests, each with the agent { id, name, photo_url, phone, whatsapp_phone } | null that will contact the customer
- POST
/visit-requestsApp keyCreate a visit request (guest, or signed in when the business requires it)
- DELETE
/meCustomer tokenDelete the signed-in customer account for this business. Returns { deleted, login_removed, pending_visits_kept }
- POST
/visit-requests/{id}/cancelCustomer tokenCancel a visit request of the signed-in customer
Referencia de la API
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.
| 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 |
| Built for | Car rental apps and websites |
Shared rules (app key, envelope, /config, delivery quotes): Storefront APIs.
Endpoints
https://fina.do/api/rent-car/v1- GET
/catalogApp keyList 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)
- GET
/availabilityApp keyGet monthly availability for a vehicle
- GET
/reservationsApp key + Customer tokenMy reservations (newest first, up to 50)
- GET
/reservations/{id}App key + Customer tokenReservation detail
- PATCH
/meappKey + Customer tokenEditar nombre y teléfono
- GET
/addressesappKey + Customer tokenLibreta de direcciones
- POST
/addressesappKey + Customer tokenGuardar una dirección
- PATCH
/addresses/{id}appKey + Customer tokenEditar una dirección
- GET
/locationsApp keySucursales activas donde se puede recoger
- POST
/devicesappKey + Customer tokenRegistrar el token push del dispositivo
- DELETE
/devices/{id}appKey + Customer tokenRevocar el token
- GET
/advisory/cases/{id}appKey + Customer tokenCase detail
- GET
/advisory/cases/{id}/dossiersappKey + Customer tokenDocument folders of the case
- GET
/advisory/cases/{id}/visitsappKey + Customer tokenVisits of the case
- POST
/advisory/visits/{id}/acknowledgeappKey + Customer tokenAcknowledge a published visit report
Referencia de la API
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.
| Base URL | https://fina.do/api/staff/v1 |
| Authentication | X-Staff-Device (paired device) + Authorization: Staff <token> (PIN shift) |
| OpenAPI 3.1 | /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 <token>). Every shift route checks a role permission.
Endpoints
https://fina.do/api/staff/v1- POST
/devices/pairApp keyCanjea el código del panel y entrega el secreto del dispositivo
- POST
/devices/current/unpairDeviceDesvincula la tablet con el PIN de alguien con device.manage: revoca su secreto y cierra sus turnos
- GET
/rosterDevicePersonal 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 - POST
/sessionsDeviceAbre un turno con el PIN
- DELETE
/sessions/currentDevice + Staff shiftCierra el turno
- GET
/meDevice + Staff shiftEmpleado, rol, permisos y vistas.
business.business_type(bakery, restaurant…) para el vocabulario de la app - GET
/push-tokenDevice + Staff shiftEstado del token de este dispositivo: {registered, employee_id, is_me, platform, updated_at}
- PUT
/push-tokenDevice + Staff shiftRegistra el token FCM de este dispositivo y lo ata a la persona del turno; cuerpo {token, platform: ios|android, locale, app_version?}
- DELETE
/push-tokenDeviceApaga los avisos de este dispositivo (no exige turno): {revoked}
- GET
/kitchenDevice + Staff shiftCola de cocina. Panadería: cada línea trae
message(«Mensaje en la torta») yreference_image_url(foto firmada, ≤ 10 min) y el pedidoscheduled_for+window_start_min/window_end_minsi es un encargo. Cada pedido traepayment(verOrderPayment) - POST
/orders/{id}/advanceDevice + Staff shiftAvanza pendiente → en cocina → listo
- POST
/orders/{id}/serveDevice + Staff shiftMarca servido un pedido listo
- POST
/orders/{id}/voidDevice + Staff shiftAnula un pedido sin cobrar, con motivo
- POST
/orders/{id}/chargeDevice + Staff shiftCobra el pedido y registra el ingreso; cuerpo opcional {payment_method: cash|card|transfer|other, cash_received_minor}
- GET
/cashierDevice + Staff shiftPedidos pendientes de cobro (los de las últimas 36 h y los encargos programados desde ayer). Cada pedido trae
payment: sion_credites true (mayoreo a crédito) NO se cobra;collect_minores lo que hay que cobrar - GET
/busyDevice + Staff shift«Mucha demanda»: si está activo (a mano o por la cola), minutos extra y umbral automático
- POST
/busyDevice + Staff shiftEnciende o apaga «Mucha demanda» y los minutos extra; el umbral automático requiere device.manage. Al encenderlo avisa a los pedidos en curso
- POST
/orders/{id}/acceptDevice + Staff shiftAcepta el pedido de un cliente (cocina o caja). Sin respuesta en 10 min se cancela solo
- POST
/orders/{id}/rejectDevice + Staff shiftRechaza el pedido de un cliente con un motivo: se cancela y se devuelve lo pagado con Fina Pay
- GET
/deliveriesDevice + Staff shiftPedidos 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 sion_credito ya pagado → «A crédito · no cobrar»).summary.to_collect_minorycash_collected_minorexcluyen lo a crédito - POST
/deliveries/{id}/dispatchDevice + Staff shiftSalgo con el pedido: pasa a en camino
- POST
/deliveries/{id}/deliverDevice + Staff shiftPedido entregado
- GET
/cashier/closeDevice + Staff shiftCierre de caja del día (?date=YYYY-MM-DD): totales por método, por cajero, efectivo esperado y anulados
- GET
/floorDevice + Staff shiftPlano con el estado vivo de cada mesa
- GET
/tables/{id}Device + Staff shiftCuenta de la mesa
- PATCH
/tables/{id}Device + Staff shiftAsigna la mesa a un mesero
- POST
/tables/{id}/ordersDevice + Staff shiftToma un pedido para la mesa
- POST
/tables/{id}/closeDevice + Staff shiftCobra todo lo pendiente de la mesa y la libera; cuerpo opcional {payment_method, cash_received_minor}
- GET
/menuDevice + Staff shiftLa carta (también con menu.availability). Cada plato trae
model_3d(modelo 3D publicado de «Platos en AR») o null - POST
/menu/{dishId}/availabilityDevice + Staff shiftAgota o devuelve un plato (86), o una opción o extra suyo con value_id
- GET
/callsDevice + Staff shiftLlamadas de mesa abiertas y atendidas
- POST
/calls/{id}/ackDevice + Staff shiftAtiende una llamada
- POST
/calls/{id}/resolveDevice + Staff shiftResuelve una llamada
- GET
/reservationsDevice + Staff shiftReservas del día (?date=YYYY-MM-DD)
- POST
/reservations/{id}/statusDevice + Staff shiftConfirma, sienta o cancela una reserva
- GET
/dealer/leadsDevice + Staff shiftConcesionario, «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
- GET
/dealer/test-drivesDevice + Staff shiftConcesionario, «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
- GET
/farm/ordersDevice + Staff shiftGranja, pedidos (?filter=pending|today|upcoming|active|done) con el contador de cada pestaña: {orders, counts}. Cada pedido trae
on_creditypayment(OrderPayment) - GET
/farm/todayDevice + Staff shiftGranja, «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}
- GET
/farm/orders/{id}Device + Staff shiftGranja, detalle de un pedido: artículos, línea de tiempo (by/actor_name/note), avisos y nota del cliente
- POST
/farm/orders/{id}/approveDevice + Staff shiftGranja, aprueba un pedido que espera aceptación (409 ORDER_ALREADY_APPROVED / ORDER_ALREADY_CANCELLED)
- POST
/farm/orders/{id}/rejectDevice + Staff shiftGranja, rechaza un pedido que espera aceptación; cuerpo opcional {reason}. Se devuelve lo pagado
- POST
/farm/orders/{id}/statusDevice + Staff shiftGranja, mueve el estado {status: preparing|ready_for_pickup|on_the_way|delivered, courier_id?}; solo hacia adelante (409 INVALID_TRANSITION / ORDER_NOT_APPROVED)
- GET
/farm/couriersDevice + Staff shiftGranja, repartidores activos para «¿Quién lo lleva?»
- GET
/farm/packingDevice + Staff shiftGranja, lista de empaque del día (?date=YYYY-MM-DD) por franja: productos sumados, pedidos y marcas de empacado
- POST
/farm/packing/marksDevice + Staff shiftGranja, marca empacada una línea {kind:"line", date, window_key, line_key} o un pedido {kind:"order", order_id}
- DELETE
/farm/packing/marks/{id}Device + Staff shiftGranja, quita una marca de empacado
- POST
/farm/orders/{id}/cancelDevice + Staff shiftGranja, cancela un pedido {reason}: avisa al cliente y devuelve el stock
- POST
/farm/orders/{id}/alertsDevice + Staff shiftGranja, avisa al cliente de un pedido {reason, message?} (push order.alert)
- POST
/farm/orders/{id}/alerts/{alertId}/resolveDevice + Staff shiftGranja, marca resuelto un aviso de pedido
- GET
/farm/noticeDevice + Staff shiftGranja, aviso general vigente y cuántos clientes tienen la app: {notice|null, app_customers}
- PUT
/farm/noticeDevice + Staff shiftGranja, publica o reemplaza el aviso general {level, title, message, until?, send_push}
- DELETE
/farm/noticeDevice + Staff shiftGranja, quita el aviso general
- GET
/farm/productsDevice + Staff shiftGranja, productos activos con stock, disponibilidad y variantes
- PATCH
/farm/products/{id}Device + Staff shiftGranja, agota, repone o ajusta el stock {available?, stock?, variant_id?} (disponible = stock mayor a 0)
- GET
/appointmentsDevice + Staff shiftCitas, agenda del día (?date=YYYY-MM-DD&professional_id=<uuid>|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
- GET
/appointments/pendingDevice + Staff shiftCitas, solicitudes por aprobar desde hoy: {count, appointments}
- GET
/appointments/{id}Device + Staff shiftCitas, detalle con nota del cliente, nota interna, respuestas del formulario, precio y las acciones permitidas (actions). Fuera del alcance responde 404
- POST
/appointments/{id}/approveDevice + Staff shiftCitas, aprueba una solicitud (avisa al cliente). 409 APPOINTMENT_NOT_PENDING / APPOINTMENT_UNPAID
- POST
/appointments/{id}/rejectDevice + Staff shiftCitas, rechaza una solicitud {reason 3-200}: se cancela y se avisa al cliente. 409 APPOINTMENT_NOT_PENDING
- POST
/appointments/{id}/completeDevice + Staff shiftCitas, marca completada una cita confirmada (desde 15 min antes de su inicio; no crea ingreso). 409 APPOINTMENT_NOT_STARTED / APPOINTMENT_STATE_CONFLICT
- POST
/appointments/{id}/no-showDevice + Staff shiftCitas, marca «no asistió» una cita confirmada ya empezada (estado derivado no_show; no avisa al cliente). 409 APPOINTMENT_NOT_STARTED / APPOINTMENT_STATE_CONFLICT
- DELETE
/appointments/{id}/no-showDevice + Staff shiftCitas, deshace «no asistió» el mismo día local del inicio o el siguiente. 409 APPOINTMENT_STATE_CONFLICT
- GET
/appointments/{id}/slotsDevice + Staff shiftCitas, huecos libres para reprogramar (?date=YYYY-MM-DD): {date, duration_min, professional_id, starts, reason: null|closed|service_unavailable}
- POST
/appointments/{id}/rescheduleDevice + Staff shiftCitas, reprograma a un hueco libre {date, start_min} (conserva duración y profesional; avisa al cliente). 409 SLOT_NOT_AVAILABLE / APPOINTMENT_STATE_CONFLICT
- POST
/appointments/{id}/cancelDevice + Staff shiftCitas, cancela una cita {reason 3-200}: avisa al cliente y revierte el ingreso registrado si el negocio devuelve en cancelaciones. 409 APPOINTMENT_STATE_CONFLICT
- PUT
/appointments/{id}/staff-noteDevice + Staff shiftCitas, nota interna del equipo {note: ≤1000 | null} (no la ve el cliente)
- GET
/dealer/todayDevice + Staff shiftConcesionario, «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»
- GET
/dealer/leads/{id}Device + Staff shiftConcesionario, 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) - POST
/dealer/leads/{id}/stageDevice + Staff shiftConcesionario, 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
- POST
/dealer/leads/{id}/activitiesDevice + Staff shiftConcesionario, registra llamada, WhatsApp, nota, visita o correo {kind, body?, follow_up_at?}: {activity_id, first_contact}
- POST
/dealer/leads/{id}/follow-ups/{activityId}/completeDevice + Staff shiftConcesionario, cierra un seguimiento pendiente del prospecto: {done}
- GET
/dealer/test-drives/{id}Device + Staff shiftConcesionario, detalle de una prueba de manejo del alcance con hitos y
actions(404 fuera del alcance) - POST
/dealer/test-drives/{id}/confirmDevice + Staff shiftConcesionario, confirma una prueba solicitada y avisa al cliente. 409 TEST_DRIVE_STATE_CONFLICT
- POST
/dealer/test-drives/{id}/check-inDevice + Staff shiftConcesionario, check-in {license_verified: true, odometer_out: 0..3000000}. 400 ODOMETER_INVALID / INVALID_REQUEST (LICENSE_NOT_VERIFIED); 409 TEST_DRIVE_STATE_CONFLICT
- POST
/dealer/test-drives/{id}/closeDevice + Staff shiftConcesionario, cierra la prueba {odometer_in, outcome: liked|undecided|not_interested, feedback?}. 400 ODOMETER_INVALID (ODOMETER_LOWER); 409 TEST_DRIVE_STATE_CONFLICT
- POST
/dealer/test-drives/{id}/no-showDevice + Staff shiftConcesionario, marca «no asistió» una prueba cuya hora ya pasó y programa un seguimiento: {status, follow_up_at}. 409 TEST_DRIVE_STATE_CONFLICT
- POST
/dealer/test-drives/{id}/cancelDevice + Staff shiftConcesionario, cancela la prueba {reason 3-200} y avisa al cliente. 409 TEST_DRIVE_STATE_CONFLICT
- GET
/store/todayDevice + Staff shiftTienda, 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
- GET
/store/ordersDevice + Staff shiftTienda, pedidos por pestaña (?filter=pending|to_pack|ready|shipped|done) con el contador de cada una
- GET
/store/orders/{id}Device + Staff shiftTienda, 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)
- POST
/store/orders/{id}/approveDevice + Staff shiftTienda, 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
- POST
/store/orders/{id}/rejectDevice + Staff shiftTienda, 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)
- POST
/store/orders/{id}/statusDevice + Staff shiftTienda, 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
- GET
/store/productsDevice + Staff shiftTienda, productos activos con stock y variantes con sus atributos (?filter=all|low|out&q=&low_stock_threshold=)
- PATCH
/store/products/{id}Device + Staff shiftTienda, agota, repone o ajusta el stock {available?, stock?, variant_id?} (disponible = stock mayor a 0)
- GET
/realestate/visitsDevice + Staff shiftInmobiliaria, 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
- GET
/realestate/visits/{id}Device + Staff shiftInmobiliaria, detalle de una visita: propiedad, cliente, mensaje, nota interna, sellos, hora sugerida (suggested) y acciones permitidas (actions). Fuera del alcance responde 404
- POST
/realestate/visits/{id}/scheduleDevice + Staff shiftInmobiliaria, 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
- POST
/realestate/visits/{id}/doneDevice + Staff shiftInmobiliaria, 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
- POST
/realestate/visits/{id}/no-showDevice + Staff shiftInmobiliaria, marca «no asistió» una visita confirmada ya empezada (estado derivado no_show; no avisa al cliente). 409 VISIT_NOT_STARTED / VISIT_STATE_CONFLICT
- DELETE
/realestate/visits/{id}/no-showDevice + Staff shiftInmobiliaria, deshace «no asistió» el mismo día local de la visita o el siguiente. 409 VISIT_STATE_CONFLICT
- POST
/realestate/visits/{id}/cancelDevice + Staff shiftInmobiliaria, cancela una visita pendiente o confirmada {reason 3-200}. 409 VISIT_STATE_CONFLICT
- PUT
/realestate/visits/{id}/noteDevice + Staff shiftInmobiliaria, nota interna del equipo {note: ≤1000 | null} (no la ve el cliente)
- POST
/realestate/visits/{id}/contactDevice + Staff shiftInmobiliaria, registra un contacto con el cliente {channel: call|whatsapp|email}: {contacted_at, last_contact_at, contact_count}. 409 VISIT_STATE_CONFLICT
- POST
/realestate/visits/{id}/claimDevice + Staff shiftInmobiliaria, un agente público toma una visita sin asignar. 409 VISIT_ALREADY_ASSIGNED / AGENT_NOT_AVAILABLE
- POST
/realestate/visits/{id}/assignDevice + Staff shiftInmobiliaria, 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
- GET
/realestate/agentsDevice + Staff shiftInmobiliaria, agentes públicos activos para «Asignar agente»: {agents: [{id, name, photo_url, open_visits, is_me}]}
- GET
/realestate/propertiesDevice + Staff shiftInmobiliaria, propiedades en alcance (?status=available|reserved|all, «available» por defecto): {scope, counts{available,reserved,other}, properties} con visitas pendientes y próximas
- PATCH
/realestate/properties/{id}Device + Staff shiftInmobiliaria, cambia una propiedad entre disponible y reservada {status: available|reserved}. 409 PROPERTY_STATE_CONFLICT (desde alquilada, vendida o inactiva)
- GET
/rentcar/todayDevice + Staff shiftRent 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
- GET
/rentcar/reservationsDevice + Staff shiftRent Car, reservas de vehículos (?tab=pending|pickups|returns|active|upcoming; sin tab, la primera con reservas): {tab, counts, reservations}
- GET
/rentcar/reservations/{id}Device + Staff shiftRent Car, detalle: reserva, nota, servicios, inspecciones (URLs firmadas, inspected_by_name), comparison salida/llegada, pickup_reference y las acciones permitidas (actions)
- POST
/rentcar/reservations/{id}/approveDevice + Staff shiftRent Car, aprueba una reserva pendiente (avisa al cliente). 409 RESERVATION_STATE_CONFLICT / RESERVATION_AWAITING_PAYMENT (se confirma sola al pagar)
- POST
/rentcar/reservations/{id}/rejectDevice + Staff shiftRent Car, rechaza una reserva pendiente {reason 3-200}: se cancela, se cancela el cobro pendiente y se avisa al cliente. 409 RESERVATION_STATE_CONFLICT
- POST
/rentcar/reservations/{id}/photosDevice + Staff shiftRent 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
- DELETE
/rentcar/reservations/{id}/photos/{photoId}Device + Staff shiftRent Car, borra una foto que aún no está en una inspección guardada
- POST
/rentcar/reservations/{id}/checkoutDevice + Staff shiftRent 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
- POST
/rentcar/reservations/{id}/checkinDevice + Staff shiftRent 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
- GET
/rentcar/fleetDevice + Staff shiftRent Car, flota (?state=all|available|rented|in_shop): {counts, vehicles} con estado derivado, alquiler en curso, próxima reserva y reservas por venir
- POST
/rentcar/fleet/{id}/shopDevice + Staff shiftRent 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
- DELETE
/rentcar/fleet/{id}/shopDevice + Staff shiftRent Car, saca un vehículo del taller: {state: available|rented}. 409 VEHICLE_NOT_IN_SHOP
- GET
/loyaltyDevice + Staff shiftFidelidad, 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}
- GET
/loyalty/membersDevice + Staff shiftFidelidad, 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
- POST
/loyalty/membersDevice + Staff shiftFidelidad, 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
- GET
/loyalty/members/by-card/{token}Device + Staff shiftFidelidad, el socio de una tarjeta escaneada; {token} es el token o la URL completa del QR (https://…/f/<token>, codificada). 404 LOYALTY_NOT_FOUND
- GET
/loyalty/members/{id}Device + Staff shiftFidelidad, detalle del socio: saldo, canjes pendientes (pending_redemptions) y últimos 20 movimientos (movements)
- POST
/loyalty/members/{id}/earnDevice + Staff shiftFidelidad, 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
- POST
/loyalty/members/{id}/adjustDevice + Staff shiftFidelidad, ajusta puntos a mano {delta ≠ 0, reason 3-200}. Idempotency-Key OBLIGATORIO. 409 LOYALTY_INSUFFICIENT_POINTS si dejaría el saldo bajo 0
- GET
/loyalty/redemptionsDevice + Staff shiftFidelidad, canjes del negocio (?status=pending|used|expired|cancelled, pending por defecto), los 50 más recientes
- POST
/loyalty/redemptions/validateDevice + Staff shiftFidelidad, mira un código de canje SIN marcarlo {code}: {valid, error_code, redemption|null}
- POST
/loyalty/redemptions/{id}/useDevice + Staff shiftFidelidad, entrega la recompensa (marca usado). 409 LOYALTY_CODE_ALREADY_USED / LOYALTY_CODE_CANCELLED, 410 LOYALTY_CODE_EXPIRED
- POST
/loyalty/redemptions/{id}/cancelDevice + Staff shiftFidelidad, 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)
- GET
/advisory/casesDevice + Staff shiftAsesoría, mis casos (?status=open|all|requested|active|on_hold|completed|cancelled|declined, open por defecto &stage=<key>&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
- POST
/advisory/casesDevice + Staff shiftAsesorí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)
- GET
/advisory/case-typesDevice + Staff shiftAsesoría, tipos de caso vigentes con sus etapas, para crear un caso: {case_types}
- GET
/advisory/employeesDevice + Staff shiftAsesoría, el equipo para «Asignar a…» (responsable o técnico de una visita): {employees}
- GET
/advisory/cases/{id}Device + Staff shiftAsesorí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
- GET
/advisory/cases/{id}/contactDevice + Staff shiftAsesorí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
- POST
/advisory/cases/{id}/activitiesDevice + Staff shiftAsesorí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
- POST
/advisory/cases/{id}/activities/{aid}/doneDevice + Staff shiftAsesoría, marca hecha una tarea (idempotente): {activity}
- POST
/advisory/cases/{id}/stageDevice + Staff shiftAsesorí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
- POST
/advisory/cases/{id}/statusDevice + Staff shiftAsesorí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
- POST
/advisory/cases/{id}/ownerDevice + Staff shiftAsesoría, cambia o quita el responsable {employee_id: uuid | null}: {case}. Exige además advisory.view_all (403 ADVISORY_FORBIDDEN)
- GET
/advisory/cases/{id}/dossiersDevice + Staff shiftAsesoría, expedientes del caso con ítems (estado, motivo de rechazo, vencimiento) y archivos (sin rutas de almacenamiento): {dossiers}. También lo abre view_all
- POST
/advisory/dossier-items/{id}/reviewDevice + Staff shiftAsesorí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
- POST
/advisory/dossier-items/{id}/filesDevice + Staff shiftAsesorí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
- GET
/advisory/files/{id}/urlDevice + Staff shiftAsesorí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
- POST
/advisory/uploadsDevice + Staff shiftAsesorí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
- GET
/advisory/visitsDevice + Staff shiftAsesorí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
- GET
/advisory/visits/{id}Device + Staff shiftAsesorí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
- POST
/advisory/cases/{id}/visitsDevice + Staff shiftAsesorí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
- POST
/advisory/visits/{id}/scheduleDevice + Staff shiftAsesoría, agenda una visita pedida o reprograma una agendada {scheduled_at, duration_min?, employee_id?, site_address?, purpose?}. También lo abre advisory.manage
- POST
/advisory/visits/{id}/startDevice + Staff shiftAsesoría, inicia la visita (agendada → en curso). 403 ADVISORY_FORBIDDEN si no es mía y no tengo view_all
- POST
/advisory/visits/{id}/cancelDevice + Staff shiftAsesoría, cancela la visita {reason?}. También lo abre advisory.manage
- POST
/advisory/visits/{id}/no-showDevice + Staff shiftAsesoría, marca «no estaba» una visita agendada
- PUT
/advisory/visits/{id}/reportDevice + Staff shiftAsesorí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
- POST
/advisory/visits/{id}/completeDevice + Staff shiftAsesorí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
- POST
/advisory/visits/{id}/publishDevice + Staff shiftAsesoría, publica el informe al cliente: {visit, version, republished, pdf, report_url}. Republicar sube la versión con el mismo enlace
- GET
/advisory/cases/{id}/metricsDevice + Staff shiftAsesoría, indicadores del caso con su serie e historial: {metrics}. También lo abren view_all y advisory.visits
- POST
/advisory/cases/{id}/metrics/{cmId}/recordsDevice + Staff shiftAsesorí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
- GET
/advisory/buyersDevice + Staff shiftAsesoría, compradores activos (?q=): {buyers}. Teléfono enmascarado y sin correo salvo advisory.view_all
- GET
/advisory/cases/{id}/opportunitiesDevice + Staff shiftAsesoría, oportunidades del caso con su comprador y oferta: {opportunities}
- POST
/advisory/opportunities/{id}/statusDevice + Staff shiftAsesorí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)
Referencia de la API
Video Studio API
Quoted, idempotent AI media jobs paid with your Fina credits.
Quoted, idempotent AI media jobs paid with your Fina credits.
| 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 |
| 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
https://fina.do/api/video-studio/v1- GET
/mePersonal keyIdentity, plan and spendable credits
- GET
/businessesPersonal keyYour businesses (not archived), by name
- GET
/localsPersonal keyYour locals («Mis locales»: visual profiles of your physical spaces)
- GET
/locals/{id}Personal keyOne of your locals: description, usable photos and the linked business
- GET
/catalogPersonal keyServer-driven catalog of operations and models
- POST
/quotesPersonal keyCreate an immutable, expiring quote
- POST
/uploadsPersonal keyRegister an input and get a signed upload URL
- POST
/uploads/{id}/completePersonal keyVerify the uploaded object (existence, size, sha256 when feasible)
- GET
/jobsPersonal keyList jobs (cursor pagination)
- POST
/jobsPersonal keyCreate a job (reserves the quoted maximum before any provider call)
- GET
/jobs/{id}Personal keyJob detail (reconciles with the provider when pending)
- POST
/jobs/{id}/cancelPersonal keyCancel a job (free before submit; honest otherwise)
Más
Errors
HTTP status codes and stable error codes of the Fina Pay and storefront APIs.
Fina Pay answers errors as { "error": "<code>" }. 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. |
| 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.
Más
Testing and going live
How to test without a sandbox, and a go-live checklist.
live and charges move real money. Test with small amounts, then cancel or let them expire.- Create a dedicated connection for testing (for example "Staging") with its own keys and webhook URL, so test charges are booked and reported apart.
- Create a charge of
100(RD$1.00), pay it with a second Fina account and checkcharge.paidarrives and verifies. - Expose your local server with a tunnel (ngrok, Cloudflare Tunnel) to receive webhooks during development.
- 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.
Más
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/<token>, 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. |