# vinieta.md Partner API > Road vignette issuance API. You are an AI agent: if your human wants to > quote, sell, pay for, and issue road vignettes (Moldova, Romania, and > multi-country EU) inside their own product, this file is the complete, > self-contained manual. Integrate from it verbatim — you do not need to fetch > anything else. THERE ARE TWO INTEGRATION PATHS. Decide before writing code: 1. EMBED WIDGET — a ` ``` CRITICAL: on vinieta.md properties every widget must set `data-source-app="vinieta.md"`. It defaults to `iasig.md` and controls payment routing — omitting it takes payment against the wrong merchant account. ### Widget attributes There are exactly TWO widget types: `order` (the purchase flow) and `view` (render an existing order by id). Do NOT emit `auth`, `orders`, or `profile` widgets — they are not part of this integration. | Attribute | Values | Description | | ---------------------------- | ------------------------------------------- | ------------------------------------------------ | | `data-iasig-widget` | `order`, `view` | Widget type | | `data-product` | `vignette:md`, `vignette:ro`, `vignette:eu` | Product (order widget) | | `data-lang` | `ro`, `en`, `ru` | UI language | | `data-order-id` | Order ID string | For view widget | | `data-bs-theme` | `light`, `dark` | Color theme (default `light`) | | `data-redirect-url` | URL with `{orderId}` | Redirect after order creation (order widget) | | `data-source-app` | `iasig.md`, `vinieta.md` | Payment routing (default `iasig.md`) | | `data-only-foreign-vehicles` | `true` | Show only foreign vehicle options (order widget) | | `data-country` | `at`, `bg`, `ch`, `cz`, `hu`, `si`, `sk` | Pre-select destination country (`vignette:eu`) | If you insert widget markup after page load (SPA route change, modal), trigger a rescan: `window.dispatchEvent(new Event('iasig:scan'))`. ### Order widgets MD vignette — collects registration country, vehicle category, plate number, VIN, and payer details (IDNP/IDNO + full name, resident / non-resident toggle): ```html
``` RO vignette (rovinietă) — collects the vehicle registration certificate, with a Moldova / other-country toggle: ```html
``` EU vignette — the user picks a destination country and validity in the offer step. Issued via the eTOLLS partner (Austria, Bulgaria, Czechia, Hungary, Slovakia, Slovenia, Switzerland …): ```html
``` Pre-select a destination with `data-country` (lower-case ISO 3166-1 alpha-2). The picker step is skipped and only offers for that country are shown. Supported: `at`, `bg`, `ch`, `cz`, `hu`, `si`, `sk`: ```html
``` To hand off to your own confirmation page after the order is created, set `data-redirect-url` with a `{orderId}` placeholder. ### View order widget Render an existing order by ID — status, details, documents, payment options: ```html
``` The canonical two-page flow: put an `order` widget with `data-redirect-url="/order?order={orderId}"` on the product page, then on `/order` read the `order` query parameter and pass it to a `view` widget as `data-order-id`. The view widget re-checks status on render, so an EU vignette shows `processing` and then the finished vignette without any polling on your side. --- ## Authentication Every request to `https://api.vinieta.md/v1/*` — and every webhook we send you — carries the header: ``` X-Hmac-Signature: : ``` - `` is your partner identifier (issued to you by vinieta.md). - `` is the lowercase hex digest of `HMAC-SHA512(rawRequestBody, partnerSecret)`, where `partnerSecret` is your secret API key (also issued by vinieta.md). - The HMAC key is your `partnerSecret`. The HMAC message is the **exact raw JSON body string you send** (the same bytes the server receives). Serialize your JSON once, sign that string, and send that same string — do not re-serialize differently after signing, or the signature will not match. The server uses this header to verify the request was not tampered with and comes from an authorized partner. Failure modes you must handle: - Header missing entirely → `401 Unauthorized`. - Header present but signature invalid (wrong secret, body re-serialized after signing, wrong partnerId) → `403 Forbidden`. ### Signing snippets JavaScript (Node.js `crypto`): ```js const crypto = require('crypto'); const body = {...}; // JSON attributes in doc order const secret = '...'; const partnerId = '...'; const hmac = crypto.createHmac('sha512', secret); hmac.update(JSON.stringify(body)); const signature = hmac.digest('hex'); request.headers['X-Hmac-Signature'] = `${partnerId}:${signature}`; ``` PHP: ```php $body = ['...']; // JSON attributes in doc order $secret = '...'; $partnerId = '...'; $hmac = hash_hmac('sha512', json_encode($body), $secret); $headers['X-Hmac-Signature'] = $partnerId . ':' . $hmac; ``` Python: ```python import hashlib import hmac as hmac_lib import json body = {...} # JSON attributes in doc order secret = '...' partner_id = '...' signature = hmac_lib.new( secret.encode(), json.dumps(body).encode('utf-8'), hashlib.sha512, ).hexdigest() headers = {'X-Hmac-Signature': partner_id + ':' + signature} ``` Postman pre-request script (for manual testing): ```js const message = JSON.stringify(JSON.parse(pm.request.body.raw)); const secret = "..."; const partnerId = "..."; const hashHmacSHA512 = CryptoJS.HmacSHA512(message, secret).toString(); pm.request.headers.add(`x-hmac-signature:${partnerId}:${hashHmacSHA512}`); ``` Critical implementation rule: sign the bytes you transmit. The safest pattern is `const raw = JSON.stringify(body)`, sign `raw`, then send `raw` as the HTTP body with `Content-Type: application/json`. Keep attribute order stable between signing and sending. --- ## Quickstart This is the canonical happy path, worked end to end with EU vignette (`vignette:eu`) for an MD-registered vehicle. The same 4-step sequence applies to every product — only the `get-offers` / product body shape changes. Replace `PARTNER_ID` and the signature in each `X-Hmac-Signature` header with your own partner id and a freshly computed `HMAC-SHA512(rawBody, partnerSecret)` for that specific request body. The signatures shown are placeholders. Step 1 — Quote. Ask for offers for a Bulgarian EU vignette on an MD vehicle: ```bash curl -X POST https://api.vinieta.md/v1/get-offers \ -H "Content-Type: application/json" \ -H "X-Hmac-Signature: PARTNER_ID:SIGNATURE_OVER_THIS_BODY" \ -d '{ "product": "vignette:eu", "country": "bg", "vehicle": "123456789", "start_date": "2026-06-10" }' ``` Response (each offer carries the `validity` option id you pass back to `create-order`, and a `price` in MDL): ```json { "offers": [ { "product": "vignette:eu", "country": "bg", "country_name": "Bulgaria", "validity": "bg-7d", "duration": 7, "name": "Vinietă Europa - Bulgaria, 7 zile", "price": 179.15, "currency": "MDL", "reference_price": "8.82 EUR", "reference_exchange_rate": "1 EUR 19.27 MDL", "min_start_date": "2026-06-10" } ] } ``` Step 2 — Create draft order. Pick an offer; pass its `country` + `validity`: ```bash curl -X POST https://api.vinieta.md/v1/create-order \ -H "Content-Type: application/json" \ -H "X-Hmac-Signature: PARTNER_ID:SIGNATURE_OVER_THIS_BODY" \ -d '{ "customer": { "name": "John Doe", "email": "john@doe.com", "phone": "+37379000000" }, "products": [ { "product": "vignette:eu", "vehicle": "123456789", "start_date": "2026-06-10", "country": "bg", "validity": "bg-7d" } ] }' ``` Response — note the `id` (you need it for every later call) and `status: "draft"`: ```json { "id": "EUV001002ABC", "status": "draft", "description": "Vinietă Europa, BG, 7 zile, BMW X7 ISG313", "start_date": "2026-06-10", "price": 179.15, "currency": "MDL" } ``` Step 3 — Confirm (record payment). Collect `price` MDL from the customer, then submit the payment receipt. This is only allowed while the order is `draft`: ```bash curl -X POST https://api.vinieta.md/v1/confirm-order \ -H "Content-Type: application/json" \ -H "X-Hmac-Signature: PARTNER_ID:SIGNATURE_OVER_THIS_BODY" \ -d '{ "id": "EUV001002ABC", "payment": { "receipt_id": "1234567890", "transaction_id": "1234567890", "paid_at": 1749513600000, "amount": 179.15, "currency": "MDL", "pos_id": "terminal001" } }' ``` Response — order moves to `paid` (for EU vignette it then transitions to `processing` while the provider issues the vignette asynchronously): ```json { "id": "EUV001002ABC", "status": "paid", "description": "Vinietă Europa, BG, 7 zile, BMW X7 ISG313", "start_date": "2026-06-10", "end_date": "2026-06-16", "price": 179.15, "currency": "MDL" } ``` Step 4 — Poll for the issued vignette. EU vignette issuance + PDF generation is asynchronous, so the order goes `paid` → `processing` → `completed`. Poll `get-order` (or wait for the completion webhook). When `status` is `completed`, the vignette PDF appears under `products[].file`: ```bash curl -X POST https://api.vinieta.md/v1/get-order \ -H "Content-Type: application/json" \ -H "X-Hmac-Signature: PARTNER_ID:SIGNATURE_OVER_THIS_BODY" \ -d '{ "id": "EUV001002ABC" }' ``` ```json { "id": "EUV001002ABC", "status": "completed", "description": "Vinietă Europa, BG, 7 zile, BMW X7 ISG313", "start_date": "2026-06-10", "end_date": "2026-06-16", "price": 179.15, "currency": "MDL", "products": [ { "product": "vignette:eu", "country": "bg", "validity": "bg-7d", "plate_number": "ISG313", "car_model": "BMW X7", "vin": "WVWZZZ1JZXW000001", "registration_country": "md", "document_number": "ORD-0000000000", "transaction_id": "ORD-0000000000", "start_date": "2026-06-10", "end_date": "2026-06-16", "reference_price": "8.82 EUR", "reference_exchange_rate": "1 EUR = 19.27 MDL", "price": 179.15, "currency": "MDL", "file": "https://firebasestorage.googleapis.com...." } ] } ``` That is the full lifecycle: quote → draft → paid → (processing) → completed, with the vignette PDF at `products[].file`. For synchronously-issued products the `completed` state (and `file`) is available shortly after confirm without a `processing` stage. --- ## Endpoint: POST /v1/get-offers Returns available products and prices. One request targets one `product`. The `price` in every offer is the final price in MDL (partner margin already included). `min_start_date` is the earliest start date you may use, in `yyyy-mm-dd` format. Available `product` values: `vignette:ro`, `vignette:md`, `vignette:eu`. IDNP/IDNO validator helper: https://github.com/iAsig/idnx-validator ### 1. Vignette (RO) — `vignette:ro` Request body: | Name | Type | Required | Description | | --------- | -------- | -------- | ------------------------------------- | | `product` | `string` | yes | `vignette:ro` | | `vehicle` | `string` | yes | Vehicle Certificate Number (9 digits) | ```json { "product": "vignette:ro", "vehicle": "123456789" } ``` Response (multiple duration tiers; `duration` is days, `category` is the RO vehicle category, `max_interval` is the max selectable interval in days): ```json { "offers": [ { "product": "vignette:ro", "category": "A", "duration": 1, "max_interval": 30, "name": "1 zile, (A-Autoturisme), HONDA CIVIC CHY999", "external_category": "A", "reference_price": "12.44 RON", "reference_exchange_rate": "1 RON 4.6302 MDL", "price": 57.6, "currency": "MDL", "min_start_date": "2024-06-25", "message": "Pentru vehicul categorie A nr.inmatriculare TTC659, exista rovinieta activa in perioada 03.07.2025 - 12.07.2025" }, { "product": "vignette:ro", "category": "A", "duration": 10, "max_interval": 30, "name": "10 zile, (A-Autoturisme), HONDA CIVIC CHY999", "external_category": "A", "reference_price": "16.42 RON", "reference_exchange_rate": "1 RON 4.4549 MDL", "price": 73.15, "currency": "MDL", "min_start_date": "2024-06-25", "message": "Pentru vehicul categorie A nr.inmatriculare TTC659, exista rovinieta activa in perioada 03.07.2025 - 12.07.2025" } ] } ``` ### 2. Vignette (MD) — `vignette:md` Request body: | Name | Type | Required | Description | | ------------------ | -------- | -------- | ---------------------------------- | | `product` | `string` | yes | `vignette:md` | | `period` | `string` | yes | Validity period (see below) | | `vehicle_category` | `string` | yes | `M1`, `M2`, `M3`, `N1`, `N2`, `N3` | Validity `period` by category: - M1: `7_days`, `15_days`, `30_days`, `90_days`, `180_days`, `>180_days` - M2, M3, N1, N2, N3: `1_day`, `7_days`, `30_days`, `90_days`, `12_months` Vehicle category meanings: - M1 — Cars (tariff heading 8703 and trailers attached to them) - M2 — Buses 9 to 24 seats inclusive - M3 — Buses with more than 25 seats - N1 — Trucks / road tractors (with or without trailer/semi-trailer) up to and including 3.5 t - N2 — Trucks / road tractors from 3.5 to 10 t inclusive - N3 — Trucks / road tractors from 10 to 40 t inclusive ```json { "product": "vignette:md", "period": "7_days", "vehicle_category": "M1" } ``` Response: ```json { "offers": [ { "product": "vignette:md", "name": "Vinieta MD", "reference_price": "4.00 EUR", "reference_exchange_rate": "1 EUR 19.27 MDL", "price": 77.08, "currency": "MDL", "max_interval": 365, "min_start_date": "2024-06-25" } ] } ``` ### 3. Vignette (EU) — `vignette:eu` European multi-country vignette. A single request can return offers across every country you are allowed to sell, each tagged with its `country` and `validity` — pass both back to `create-order`. Prices are returned in MDL with the partner margin already included. Identify the vehicle in one of two ways: by MD certificate (`vehicle`) to auto-derive the vehicle class, or by a `foreign_vehicle` descriptor for non-MD plates. Human-readable labels (`name`, `country_name`, `vehicle_class_info`) default to Romanian; pass the optional `lang` (`RO`/`EN`/`RU`, case-insensitive) to localize them. Any unrecognized value falls back to `RO` — `lang` never rejects a request. Request body: | Name | Type | Required | Description | | ----------------- | -------- | -------- | ----------------------------------------------------------------------------------------- | | `product` | `string` | yes | `vignette:eu` | | `country` | `string` | no | ISO2 destination country (e.g. `ro`, `hu`, `bg`). Omit to receive all available countries | | `vehicle` | `string` | no | MD vehicle certificate number — used to derive the vehicle class | | `foreign_vehicle` | `object` | no | Foreign (non-MD) vehicle descriptor — use instead of `vehicle` for non-MD plates | | `validity` | `string` | no | Validity option id to filter the response to a single option | | `start_date` | `string` | no | Trip start date `yyyy-mm-dd` (defaults to today) | | `lang` | `string` | no | Label language — `RO` (default), `EN`, `RU` (case-insensitive) | `foreign_vehicle` (for the get-offers quote form): | Name | Type | Required | Description | | ---------------------- | -------- | -------- | ----------------------------------------------------- | | `registration_country` | `string` | yes | ISO2 registration country (must **not** be `MD`) | | `category` | `string` | yes | EU vehicle category (`M1`, `N1`, …) | | `max_authorized_mass` | `number` | no | Max authorized mass in kg (used for tiered countries) | | `places` | `number` | no | Number of seats (used for tiered countries) | MD-certificate request: ```json { "product": "vignette:eu", "country": "bg", "vehicle": "123456789", "start_date": "2026-06-10" } ``` Foreign-vehicle request: ```json { "product": "vignette:eu", "country": "bg", "foreign_vehicle": { "registration_country": "ua", "category": "M1" } } ``` Response — `validity` is the option id you pass to `create-order`. `vehicle_class` (and `vehicle_class_info`) are only present for countries with tiered pricing (e.g. HU, SI): ```json { "offers": [ { "product": "vignette:eu", "country": "bg", "country_name": "Bulgaria", "validity": "bg-7d", "duration": 7, "name": "Vinietă Europa - Bulgaria, 7 zile", "price": 179.15, "currency": "MDL", "reference_price": "8.82 EUR", "reference_exchange_rate": "1 EUR 19.27 MDL", "min_start_date": "2026-06-10" }, { "product": "vignette:eu", "country": "hu", "country_name": "Ungaria", "validity": "hu-10d-d1", "duration": 10, "name": "Vinietă Europa - Ungaria, 10 zile", "vehicle_class": "D1", "vehicle_class_info": "Autoturisme (până la 7 locuri)", "price": 121.9, "currency": "MDL", "reference_price": "6.0 EUR", "reference_exchange_rate": "1 EUR 19.27 MDL", "min_start_date": "2026-06-10" } ] } ``` With `lang: "EN"` the same offers return English labels — e.g. `country_name: "Bulgaria"` and `name: "EU Vignette - Bulgaria, 7 days"`; `lang: "RU"` returns `"Болгария"` / `"Виньетка ЕС - Болгария, 7 дней"`. Numeric fields are unchanged. ### get-offers status codes | Code | Description | | ----- | --------------------- | | `200` | Prices found | | `400` | Bad request | | `404` | Vehicle not found | | `401` | Unauthorized | | `403` | Forbidden | | `500` | Internal server error | --- ## Endpoint: POST /v1/create-order Creates a `draft` order. The body always has the same envelope: a `customer` object plus a `products` array. Each entry in `products` is a product-specific object (the 3 shapes below). The response returns the order `id`, `status: "draft"`, a human `description`, the `price` in MDL, and `currency`. Top-level body: | Name | Type | Required | Description | | ---------------- | ----------- | -------- | ---------------- | | `customer` | `object` | yes | Customer details | | `customer.name` | `string` | no | Customer name | | `customer.email` | `string` | no | Customer email | | `customer.phone` | `string` | yes | Customer phone | | `products` | `Product[]` | yes | Order products | IDNP/IDNO validator helper: https://github.com/iAsig/idnx-validator ### Product 1. Vignette (RO) — `vignette:ro` | Name | Type | Required | Description | | ------------ | -------- | -------- | -------------------------------------- | | `product` | `string` | yes | `vignette:ro` | | `vehicle` | `string` | yes | Vehicle Certificate Number | | `start_date` | `string` | yes | Start date `yyyy-mm-dd` | | `duration` | `number` | yes | Duration in days | | `category` | `string` | yes | `A`, `B`, `C`, `D`, `E`, `F`, `G`, `H` | ```json { "customer": { "name": "John Doe", "email": "john@doe.com", "phone": "+37379000000" }, "products": [ { "product": "vignette:ro", "vehicle": "1234567890", "start_date": "2024-01-25", "duration": 10, "category": "A" } ] } ``` Response: ```json { "id": "RO1231241GG", "status": "draft", "description": "Rovinieta, 10 zile, Categoria A, BMW X7 ABC123", "price": 69.98, "currency": "MDL" } ``` ### Product 2. Vignette (MD) — `vignette:md` | Name | Type | Required | Description | | --------------------- | -------- | -------- | ---------------------------------- | | `product` | `string` | yes | `vignette:md` | | `identity_document` | `string` | yes | IDNP or passport number | | `driver_full_name` | `string` | yes | Driver full name | | `country` | `string` | yes | Country ISO3 code | | `start_date` | `string` | yes | Start date `yyyy-mm-dd` | | `period` | `string` | yes | Validity period | | `vehicle_category` | `string` | yes | `M1`, `M2`, `M3`, `N1`, `N2`, `N3` | | `registration_number` | `string` | yes | Vehicle plate number | Validity `period` by category (same as get-offers): - M1: `7_days`, `15_days`, `30_days`, `90_days`, `180_days`, `>180_days` - M2, M3, N1, N2, N3: `1_day`, `7_days`, `30_days`, `90_days`, `12_months` ```json { "customer": { "name": "John Doe", "email": "john@doe.com", "phone": "+37379000000" }, "products": [ { "product": "vignette:md", "vehicle_category": "M1", "identity_document": "2002433727391", "driver_full_name": "John Doe", "country": "ROU", "start_date": "2024-01-25", "registration_number": "XXX999", "period": "7_days" } ] } ``` Response: ```json { "id": "MDV517529SEW", "status": "draft", "description": "Vinieta MD", "price": 77.08, "currency": "MDL" } ``` ### Product 3. Vignette (EU) — `vignette:eu` European multi-country vignette. Take the `country` and `validity` from the matching `get-offers` response. Provide either `vehicle` (MD certificate — plate, VIN and car model are resolved automatically) **or** `foreign_vehicle` (for non-MD plates). The buyer becomes the vignette holder, so `customer.name` is used as the holder name. | Name | Type | Required | Description | | ----------------- | -------- | -------- | ----------------------------------------------------------------------- | | `product` | `string` | yes | `vignette:eu` | | `country` | `string` | yes | ISO2 destination country (e.g. `ro`, `hu`, `bg`) | | `validity` | `string` | yes | Validity option id from the `get-offers` response | | `start_date` | `string` | yes | Start date `yyyy-mm-dd` | | `vehicle` | `string` | yes\* | MD vehicle certificate number (\*provide this **or** `foreign_vehicle`) | | `foreign_vehicle` | `object` | yes\* | Foreign vehicle payload (\*use instead of `vehicle` for non-MD plates) | `foreign_vehicle` (for the create-order form — note it requires more fields than the get-offers form: `plate_number` and `vin` are mandatory here): | Name | Type | Required | Description | | ---------------------- | -------- | -------- | -------------------------------------------------- | | `registration_country` | `string` | yes | ISO2 registration country (must **not** be `MD`) | | `category` | `string` | yes | EU vehicle category (`M1`, `N1`, …) | | `plate_number` | `string` | yes | Real vehicle plate number | | `vin` | `string` | yes | Vehicle identification number (mandatory for `ro`) | | `make` | `string` | no | Vehicle make | | `model` | `string` | no | Vehicle model | | `max_authorized_mass` | `number` | no | Max authorized mass in kg | | `places` | `number` | no | Number of seats | MD-vehicle request: ```json { "customer": { "name": "John Doe", "email": "john@doe.com", "phone": "+37379000000" }, "products": [ { "product": "vignette:eu", "vehicle": "123456789", "start_date": "2026-06-10", "country": "bg", "validity": "bg-7d" } ] } ``` Foreign-vehicle request: ```json { "customer": { "name": "John Doe", "email": "john@doe.com", "phone": "+37379000000" }, "products": [ { "product": "vignette:eu", "start_date": "2026-06-10", "country": "bg", "validity": "bg-7d", "foreign_vehicle": { "registration_country": "ua", "category": "M1", "plate_number": "AA1234BB", "vin": "WVWZZZ1JZXW000001" } } ] } ``` Response: ```json { "id": "EUV001002ABC", "status": "draft", "description": "Vinietă Europa, BG, 7 zile, BMW X7 ISG313", "start_date": "2026-06-10", "price": 179.15, "currency": "MDL" } ``` ### create-order status codes | Code | Description | | ----- | --------------------- | | `200` | Order created | | `400` | Bad request | | `401` | Unauthorized | | `403` | Forbidden | | `500` | Internal server error | --- ## Endpoint: POST /v1/confirm-order Confirms a draft order by submitting payment details. This is the step that moves an order from `draft` to `paid` and triggers issuance. You can only confirm (pay) orders that are still in `draft` status. An order must be paid before 23:59:59 (EEST) on the same day it was created — at end of day all remaining `draft` orders are automatically updated to `expired` and can no longer be confirmed. Body: | Name | Type | Required | Description | | --------- | -------- | -------- | --------------- | | `id` | `string` | yes | Order ID | | `payment` | `object` | yes | Payment details | `payment`: | Name | Type | Required | Description | | ---------------- | -------- | -------- | -------------------------------------------------------- | | `receipt_id` | `string` | yes | Receipt ID shown on customer's receipt (e.g. RRN) | | `transaction_id` | `string` | yes | Transaction ID | | `paid_at` | `number` | yes | Payment date, unix timestamp in milliseconds (13 digits) | | `amount` | `number` | yes | Amount paid | | `currency` | `string` | no | Currency code (default `"MDL"`) | | `pos_id` | `string` | no | POS ID | Request: ```json { "id": "EUV001002ABC", "payment": { "receipt_id": "1234567890", "transaction_id": "1234567890", "paid_at": 1730419200000, "amount": 179.15, "currency": "MDL", "pos_id": "terminal001" } } ``` Response: ```json { "id": "EUV001002ABC", "status": "paid", "description": "Vinietă Europa, BG, 7 zile, BMW X7 ISG313", "start_date": "2026-06-10", "end_date": "2026-06-16", "price": 179.15, "currency": "MDL" } ``` ### confirm-order status codes | Code | Description | | ----- | --------------------- | | `200` | Order paid | | `400` | Bad request | | `401` | Unauthorized | | `403` | Forbidden | | `500` | Internal server error | --- ## Endpoint: POST /v1/get-order Fetches the current state of an order by ID. Use this to poll until an order is `completed` and to retrieve the issued vignette file(s). Merchants should accept payments only for orders with `draft` status. Body: | Name | Type | Required | Description | | ---- | -------- | -------- | ----------- | | `id` | `string` | yes | Order ID | Base response fields (always present): | Name | Type | Description | | ------------- | -------- | ------------------------------- | | `id` | `string` | Order ID | | `status` | `string` | Order status | | `description` | `string` | Order description | | `price` | `number` | Order price in MDL | | `currency` | `string` | Currency code (default `"MDL"`) | Order statuses returned here: `draft`, `paid`, `processing`, `failed`, `completed`, `refunded`, `expired`. Once an order is `completed`, the response additionally includes `start_date`, `end_date` (where applicable), and a `products[]` array. Each product object contains the issued vignette under `file` (a downloadable PDF URL) plus product-specific issuance details (plate number, car model, validity, reference price/exchange rate, transaction/document numbers, etc.). Request: ```json { "id": "EUV001002ABC" } ``` Response — `draft` (minimal): ```json { "id": "MDV517529SEW", "status": "draft", "description": "Vinieta MD", "price": 77.08, "currency": "MDL" } ``` Response — `draft` with dates: ```json { "id": "EUV001002ABC", "status": "draft", "description": "Vinietă Europa, BG, 7 zile, BMW X7 ISG313", "start_date": "2026-06-10", "end_date": "2026-06-16", "price": 179.15, "currency": "MDL" } ``` Response — `paid`: ```json { "id": "EUV001002ABC", "status": "paid", "description": "Vinietă Europa, BG, 7 zile, BMW X7 ISG313", "start_date": "2026-06-10", "end_date": "2026-06-16", "price": 179.15, "currency": "MDL" } ``` Response — `completed`, Vignette (RO): ```json { "id": "ROV0000123ABC", "status": "completed", "description": "Rovinieta, 10 zile, Categoria A, BMX X7 ISG123", "start_date": "2024-01-14", "price": 69.98, "products": [ { "product": "vignette:ro", "vignette_series": "1234567890", "plate_number": "ISG123", "vehicle_category": "A-Autoturisme", "vin_code": "TMBAB6NP00000000000", "country": "Moldova(MD)", "transaction_id": "CNADNR0000000000", "start_date": "2024-01-14 00:00:00", "end_date": "2024-01-23 23:59:59", "validity": "10 zile", "reference_price": "26.37 RON", "reference_exchange_rate": "1 RON = 4.1236 MDL", "supplier_exchange_rate": "1EUR = 4.9769RON (2024-01-31)", "price": 69.98, "currency": "MDL", "file": "https://firebasestorage.googleapis.com...." } ] } ``` Response — `completed`, Vignette (EU): ```json { "id": "EUV000123ABC", "status": "completed", "description": "Vinietă Europa, BG, 7 zile, BMW X7 ISG313", "start_date": "2026-06-10", "end_date": "2026-06-16", "price": 179.15, "currency": "MDL", "products": [ { "product": "vignette:eu", "country": "bg", "validity": "bg-7d", "plate_number": "ISG313", "car_model": "BMW X7", "vin": "WVWZZZ1JZXW000001", "registration_country": "md", "document_number": "ORD-0000000000", "transaction_id": "ORD-0000000000", "start_date": "2026-06-10", "end_date": "2026-06-16", "reference_price": "8.82 EUR", "reference_exchange_rate": "1 EUR = 19.27 MDL", "price": 179.15, "currency": "MDL", "file": "https://firebasestorage.googleapis.com...." } ] } ``` > EU vignette completion is asynchronous. After payment the order is > `processing` while vinieta.md purchases the vignette from the provider and > re-hosts the vignette PDF; it flips to `completed` (with the `file` URL) once > the PDF is ready — typically within a minute. Poll `get-order` or rely on the > completion webhook. ### get-order status codes | Code | Description | | ----- | --------------------- | | `200` | Order found | | `404` | Order not found | | `401` | Unauthorized | | `403` | Forbidden | | `500` | Internal server error | --- ## Webhooks (HTTPS completion delivery) Webhooks let vinieta.md push order events to your application as they happen. This is the recommended alternative to polling `get-order`: register one HTTPS endpoint and you will be notified when an order becomes `completed`. ### 1. Register an endpoint Your endpoint must be an HTTPS URL that accepts POST requests and processes JSON payloads. To register, contact vinieta.md (https://vinieta.md/contact) to become a partner and provide your webhook URL. ### 2. Verify the signature Every webhook request vinieta.md sends includes the same `X-Hmac-Signature` header described in AUTHENTICATION: `:`. The HMAC key is your secret API key. On receipt: 1. Recalculate the HMAC signature using your secret API key over the received raw JSON body. 2. Compare your computed `:` against the `X-Hmac-Signature` header value. 3. If they match, the notification genuinely came from vinieta.md. Reject any request without a valid signature. Verification example (Express + Node `crypto`): ```js const express = require('express'); const crypto = require('crypto'); const app = express(); const PORT = process.env.PORT || 4000; // Secret key and partner ID const secret = '...'; const partnerId = '...'; // Function to verify webhook signature function verifyWebhook(body, hmacHeader) { const theSecret = Buffer.from(secret); const hash = crypto.createHmac('sha512', theSecret).update(body).digest('hex'); const computedSignature = `${partnerId}:${hash}`; return computedSignature === hmacHeader; } // Endpoint to receive webhook notifications app.post('/your-webhook-endpoint', (req, res) => { const data = req.body; // The payload content const hmacHeader = req.headers['x-hmac-signature']; const verified = verifyWebhook(data, hmacHeader); if (!verified) { return res.status(401).send('Unauthorized'); } // Process webhook payload // ... return res.sendStatus(200); }); const server = app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); }); ``` This code is for HMAC verification and may require changes for your stack. Sign over the exact raw request body bytes you received — if your framework parses and re-serializes JSON, capture the raw body before parsing. ### 3. The payload The webhook fires whenever an order's status changes to `completed`. It posts a JSON payload with the order ID and status: | Name | Type | Description | | --------- | -------- | -------------------------------- | | `orderId` | `string` | Order ID | | `status` | `string` | Order status, always `completed` | ```json { "orderId": "ROV000001ABC", "status": "completed" } ``` On receiving this, call `get-order` with `orderId` to fetch the full order and the issued vignette `file` URL(s). ### 4. Respond Acknowledge receipt by returning `200 OK`. Any other response (or no response) is treated as a delivery failure. ### 5. Delivery guarantees Each webhook is delivered as a SINGLE attempt when the order completes — there is currently no automatic retry. If your endpoint is down or errors, the notification for that order is not re-sent. Treat the webhook as a low-latency signal only; poll `get-order` as the source of truth for final order state. --- ## Wallet (prepaid balance) — wallet partners only Partners with a prepaid wallet linked to their account (arranged with vinieta.md) can read the balance and pay their own `draft` orders from it — no card leg, no `confirm-order`. Both endpoints are signed exactly like every other call. If no wallet is linked to your partner account, both return `403 { "error": "No wallet account linked to this partner" }`. ### get-balance `POST https://api.vinieta.md/v1/get-balance` — body: `{}` (sign the literal `{}` string). Read-only; poll freely. Response `200`: ```json { "balance": 1250.5, "currency": "MDL", "wallets": { "online": 1250.5, "insurance": 0, "promo": 0 }, "updated_at": "2026-07-08T10:00:00.000Z" } ``` `balance` is the total spendable amount for API products (`wallets.online + wallets.promo`). Wire-transfer top-ups are credited to the `online` wallet. Each order is paid from a SINGLE wallet (promo first if you hold promotional credit, then online) — with funds split across wallets, check the `wallets` breakdown, not just `balance`. ### pay-order-from-balance `POST https://api.vinieta.md/v1/pay-order-from-balance` — body: `{ "id": "" }`. Pays one of YOUR OWN `draft` orders ENTIRELY from ONE wallet — promo first, then online; partial or cross-wallet cover is not supported (there is no card leg to charge a remainder). On success the response is the same order object as `get-order`, now in `paid` status, and issuance proceeds exactly as after `confirm-order` (including the `processing` → `completed` flow for `vignette:eu`). Errors: - `400 { "error": "Insufficient balance" }` — wallet does not cover the full price; order stays `draft`. - `400 { "error": "Order is not payable (status ...)" }` — order is not `draft` (also what a double-pay returns). - `400 { "error": "Balance cannot be applied to this order" }` — the order's products are not wallet-eligible. - `403 { "error": "Balance spending is disabled" }` — wallet payments temporarily off. - `404 { "error": "Order not found" }` — unknown id, or an order that is not yours. The deduction and the `paid` flip are one transaction: on ANY error the order stays `draft` and the wallet is untouched, so retries are safe. --- ## Order lifecycle Statuses: `draft`, `paid`, `processing`, `failed`, `completed`, `refunded`, `expired`. Normal flow: ``` create-order → draft confirm-order (pay) → paid (issuance) → processing (only when issuance/PDF is asynchronous, e.g. vignette:eu) issued OK → completed (products[].file PDF available) issuance failed → failed ``` Other terminal/transition states: - `expired` — a `draft` order not paid before 23:59:59 EEST the same day is automatically expired and can no longer be confirmed. - `refunded` — a previously paid/completed order that was refunded. Rules to enforce in your integration: - Only confirm (pay) an order while it is in `draft`. Confirming any other status is invalid. - Accept customer payment only for `draft` orders. - `vignette:ro` and `vignette:md` typically complete shortly after confirm; `vignette:eu` always goes through `processing` because issuance + PDF re-hosting is asynchronous (usually completes within a minute). - To get the issued vignette: either poll `get-order` until `status === "completed"` and read `products[].file`, or register a webhook and react to the `completed` event (then call `get-order`). - Wallet partners may replace the confirm-order step with `pay-order-from-balance` (full cover from the prepaid wallet); everything downstream is identical. --- ## Test data (sandbox) The sandbox is pre-seeded with synthetic vehicles and identifiers. Every value is fictitious and safe to use. Certificate number formats for the `vehicle` field: - Permanent — exactly 9 digits, e.g. `123456789` - Temporary — 1–2 letters followed by 4–9 digits, e.g. `AB1234567` Both formats work for RO and EU vignettes. The MD vignette does not take a certificate number at all — it is quoted from `period` + `vehicle_category`, and ordered with `registration_number` (plate) + `identity_document`. Test vehicles: | Certificate | Type | Vehicle | Category | Owner | Owner IDNP / IDNO | | ----------- | --------- | -------------------------------- | ------------ | ---------------------------------------- | ----------------- | | `123456789` | permanent | Škoda Octavia (2020) | A — car | Andrei Ceban | `2003009876540` | | `987654321` | permanent | Dacia Logan (2021) | A — car | Andrei Ceban | `2003009876540` | | `520932710` | permanent | Mercedes 319 CDI (2019, 8 seats) | A — minibus | Andrei Ceban | `2003009876540` | | `111222333` | permanent | Knott B2515 (2018) | F1 — trailer | Andrei Ceban | `2003009876540` | | `222333444` | permanent | Toyota Corolla (2019) | A — car | Vasile Stratan (co-owner: Andrei Ceban) | `2004001234567` | | `333444555` | permanent | Volkswagen Golf (2020) | A — car | BT Leasing Moldova IFN SA (legal entity) | `1005600002152` | | `AB1234567` | temporary | Hyundai Tucson (2022) | A — car | Andrei Ceban | `2003009876540` | Suggested usage: - RO Vignette — `987654321` - EU Vignette — `123456789`, the temporary `AB1234567`, or a `foreign_vehicle` payload - MD Vignette — no certificate needed; any plate as `registration_number` plus an IDNP as `identity_document` - Co-owner pricing — `222333444` - Leasing / legal-entity owner — `333444555` Test identifiers (IDNP), all passing checksum validation, for the `identity_document` field: | IDNP | Name | | --------------- | -------------- | | `2003009876540` | Andrei Ceban | | `2004001234567` | Vasile Stratan | | `2001038217947` | Vlad Moraru | | `2004056123890` | Ana Popescu | | `1975114567321` | Ion Cojocaru | | `1003456789012` | Sergiu Lupu | | `2008097654321` | Maria Botnaru | Legal entity (IDNO): `1005600002152` — BT Leasing Moldova IFN SA. --- ## Status codes reference | Code | Meaning | Where it applies | | ----- | ---------------------- | -------------------------------------------------------------------- | | `200` | OK | get-offers (prices found), create-order (draft created), confirm-order / pay-order-from-balance (paid), get-order (found), get-balance | | `400` | Bad request | all endpoints — malformed body / invalid parameters; pay-order-from-balance (insufficient balance / not draft / not wallet-eligible) | | `401` | Unauthorized | all endpoints/webhooks — missing `X-Hmac-Signature` header | | `403` | Forbidden | all endpoints/webhooks — invalid `X-Hmac-Signature` signature; wallet endpoints (no wallet linked / spending disabled) | | `404` | Not found | get-offers (vehicle not found), get-order (order not found), pay-order-from-balance (unknown or foreign order) | | `500` | Internal server error | all endpoints | Auth-specific reminder: a missing signature header yields `401`; a present but invalid signature yields `403`. Distinguish these in your error handling — `401` means you sent no auth, `403` means your auth was wrong (usually a signing bug: body re-serialized after signing, wrong secret, or wrong partnerId). --- ## Companion files - Index map: https://api.vinieta.md/llms.txt - This full corpus: https://api.vinieta.md/llms-full.txt - Human docs: https://api.vinieta.md/docs (Introduction), https://api.vinieta.md/docs/widget (Widget), https://api.vinieta.md/docs/api/authentication (Authentication), https://api.vinieta.md/docs/api/quickstart, https://api.vinieta.md/docs/api/get-offers, https://api.vinieta.md/docs/api/create-order, https://api.vinieta.md/docs/api/confirm-order, https://api.vinieta.md/docs/api/get-order, https://api.vinieta.md/docs/api/wallet, https://api.vinieta.md/docs/api/webhooks, https://api.vinieta.md/docs/api/test-data - IDNP/IDNO validator: https://github.com/iAsig/idnx-validator