
# Order Flow

A read-once reference for partners building any integration with Vignette ID. This page maps the **moving parts** (your backend, our API, the national provider, your webhook endpoint) to the **events** they exchange so you can wire your system correctly the first time.

If you only have five minutes, skip to the [sequence diagram](#order-lifecycle) — it shows the full happy path from `POST /orders` to the PDF confirmation.

---

## Choose your integration

We support three integration shapes. Pick one based on how much UI control you need vs. how fast you want to ship.

<Mermaid chart={`flowchart TD
  start([Partner needs to sell e-vignettes]) --> q{How much UI<br/>control do you need?}
  q -- Full control, custom UI --> api[API Integration]
  q -- Drop-in widget, some branding --> widget[Web Widget]
  q -- Zero code, default flow --> iframe[Iframe]

  api --> apiReqs["API key + secret<br/>Webhook endpoint<br/>Backend implementation"]
  widget --> widgetReqs["API key<br/>Single script tag<br/>Optional config"]
  iframe --> iframeReqs["Partner Panel config<br/>iframe embed code<br/>Stripe handles payment"]

  apiReqs --> apiGuide([Read: API References])
  widgetReqs --> widgetGuide([Read: Widget Integration])
  iframeReqs --> iframeGuide([Read: Iframe Integration])

  classDef start fill:#e8f4f4,stroke:#01696F,stroke-width:2px,color:#01696F
  classDef path fill:#fff,stroke:#5a5e6e,stroke-width:1.5px,color:#1a1a2e
  classDef guide fill:#01696F,stroke:#01696F,color:#fff
  class start start
  class api,widget,iframe,apiReqs,widgetReqs,iframeReqs path
  class apiGuide,widgetGuide,iframeGuide guide
`} />

| Path | Best for | Time to live | Customization |
|---|---|---|---|
| **API** | Established partners with their own checkout | 2–6 weeks | Full |
| **Widget** | Travel sites, content sites, MVP launches | 1–3 days | Theme + presets |
| **Iframe** | Affiliates, blogs, no-code sites | < 1 hour | Locale + theme |

---

## End-to-end order flow

A robust integration is shown as one timeline: **pre-flight checks**, then **bounded-retry order creation**, then the **async lifecycle** driven by webhooks. Status names match [`/wiki/statuses`](/wiki/statuses); webhook events match [`/wiki/webhooks`](/wiki/webhooks).

<Mermaid chart={`sequenceDiagram
  autonumber
  actor User as End user
  participant FE as Partner frontend
  participant Backend as Partner backend
  participant API as Vignette ID API
  participant Provider as National provider
  participant Hook as Partner webhook

  rect rgb(240, 249, 249)
  Note over User,API: 1 — Page load: decide what to offer
  User->>FE: Opens vignette purchase page
  FE->>Backend: Render
  Backend->>API: GET /products/status
  API-->>Backend: per-country / per-period availability
  Note over Backend: Cache ~300 s
  Backend-->>FE: Render only AVAILABLE countries & periods
  FE-->>User: Show enabled options
  end

  rect rgb(232, 244, 244)
  Note over User,FE: 2 — User fills the form
  User->>FE: Pick country, period, enter plate + VIN
  FE->>Backend: Submit form
  end

  rect rgb(240, 249, 249)
  Note over Backend,API: 3 — Validate the plate before spending an order
  Backend->>API: POST /validate-vehicle
  API-->>Backend: { valid: true | false, reason }
  alt Plate invalid
    Backend-->>FE: Show validation error
    Note over FE,User: User fixes plate / VIN, re-submits → back to step 2
  end
  end
  rect rgb(232, 244, 244)
  Note over User,Backend: 4 — Collect payment from the user (partner-handled flow only)
  opt Partner handles payment
    User-->>Backend: Payment confirmed
  end
  end

  rect rgb(254, 249, 236)
  Note over Backend,API: 5 — Create the order (max 5 attempts, 1 min apart, same idempotency-key, order_has_been_paid true)
  Backend->>API: POST /orders (attempt N, idempotency-key)
  alt 200 — order created
    API-->>Backend: { id, status: CREATED, payment_link }
    API->>Hook: ORDER_STATUS_CHANGED → CREATED
  else 409 — already exists
    API-->>Backend: 409 with existing order id
    Note over Backend: Stop — surface existing order, don't retry
  else 4xx — bad request
    API-->>Backend: 4xx (bad plate / unknown product)
    Note over Backend: Stop — fix input, do not retry
  else 5xx / network error
    API--xBackend: error
    Note over Backend: Wait 1 min, retry — give up after 5 failures
  end
  end

  rect rgb(232, 244, 244)
  Note over Backend,Hook: 6 — Lifecycle (only on 200 — everything below assumes success)

  opt Vignette ID handles payment
    Backend->>FE: Redirect to payment_link
    User->>API: Pays via Stripe checkout
    API->>Hook: CHECKOUT_STATUS_CHANGED → SUCCESS
  end

  Note over API,Provider: Async — minutes to hours
  API->>Provider: Register vignette

  alt Start date is more than 3 days out
    API->>Hook: ORDER_STATUS_CHANGED → DEFERRED
    Note over API,Provider: Held, registered<br/>6 hours before start
  else Start date is now / soon
    API->>Hook: ORDER_STATUS_CHANGED → PENDING
  end

  Provider-->>API: Registered
  API->>Hook: ORDER_STATUS_CHANGED → WILL_BE_ACTIVE
  Provider-->>API: Active + unique ID
  API->>Hook: ORDER_STATUS_CHANGED → ACTIVE

  Note right of Hook: Vignette is valid — safe to<br/>show "Done" to user.

  Provider-->>API: Confirmation PDF
  API->>Hook: ORDER_CONFIRMATION_RECEIVED (pdf_url)
  Backend->>User: Email PDF / show download
  end
`} />

### Why each step matters

1. **`GET /products/status` on page load** — country and period availability can flip mid-day (national provider outage, maintenance window). Run this **before rendering the form**, not at submit time, so unavailable countries / durations are *hidden* in your UI rather than "click to find out". Cache the response for ~60 s — there's no reason to hammer the endpoint on every page hit.
2. **`POST /validate-vehicle` after the user enters the plate** — catches typos (Cyrillic chars, malformed VINs, missing dashes for AT/DE) on the cheap server-side check **before** spending an order attempt. Failed validation costs nothing; a failed `POST /orders` costs an idempotency slot and can leave a partial record. Run it on form submit (or onBlur of the plate field for inline UX).
3. **`POST /orders` outcomes:**
   - `200` — keep the returned `id`, persist it locally, then watch webhooks.
   - `409` (or equivalent duplicate signal) — **stop trying**. Re-querying with the same body will produce the same answer. Surface the existing order to the user.
   - `5xx` / network errors — transient. Retry **once per minute, max 5 attempts** (≈5-minute total window). After that, surface a "we couldn't process your order" screen and notify on-call.
   - `4xx` other than 409 — your request is malformed (bad plate format, invalid product). Don't retry; fix the input.

<Callout type="caution" title="Idempotency and duplicates">
  Sending the same `POST /orders` body twice during a network blip can create two orders. Use an idempotency key (or compare against your local "in-flight" record) before retrying — every retry should include the **same key** as the first attempt so the API can deduplicate.
</Callout>

### Failure & cancellation paths

<Mermaid chart={`flowchart LR
  created([CREATED]) --> pendingOrDeferred{PENDING<br/>or DEFERRED?}
  pendingOrDeferred -- pending --> pending([PENDING])
  pendingOrDeferred -- deferred --> deferred([DEFERRED])
  pending --> willBeActive([WILL BE ACTIVE])
  deferred -. 6h before start .-> willBeActive
  willBeActive --> active([ACTIVE])
  active --> expired([EXPIRED])

  pending -. cancel after 15 min .-> refunded([REFUNDED])
  deferred -. cancel up to 6h before .-> refunded
  active -. manual chargeback .-> refunded

  any[Any state] -. unknown error .-> deleted([DELETED])

  classDef happy fill:#e8f4f4,stroke:#01696F,color:#01696F
  classDef terminal fill:#fef9ec,stroke:#c87b08,color:#7c4d00
  classDef bad fill:#fdecea,stroke:#d63b3b,color:#a51c1c
  class created,pending,willBeActive,active,deferred happy
  class expired,refunded terminal
  class deleted bad
`} />

---

## Status reference

Every status you'll see in webhook payloads or `GET /orders/{id}/status`:

| Status | When you see it | What to show the user |
|---|---|---|
| `CREATED` | Right after `POST /orders` succeeds | "Processing your order…" |
| `DEFERRED` | Start date > 3 days from purchase | "Vignette scheduled for &lt;date&gt;" |
| `PENDING` | Awaiting national provider | "Processing your order…" |
| `WILL BE ACTIVE` | Provider registered, awaiting unique ID | "Almost ready…" |
| `ACTIVE` | Vignette is valid — user can drive | "✅ Vignette is active" |
| `EXPIRED` | Validity period ended naturally | "Expired on &lt;date&gt;" |
| `REFUNDED` | Cancelled or charged back | "Refunded" |
| `DELETED` | Unknown error, contact support | Show error + support link |

Full descriptions and cancellation windows: [Statuses list](/wiki/statuses).

---

## Don't poll — listen for webhooks

`GET /orders/{id}/status` is rate-limited to **15–25 req/min per order ID** ([rate limits](/wiki/rate-limits)) precisely because polling is the wrong tool here.

<Callout type="tip" title="Recommended">
  Configure your webhook URL in the Partner Panel and update your local order record on every `ORDER_STATUS_CHANGED`. Use `GET /orders/{id}/status` only as a manual fallback (e.g. an admin "refresh" button).
</Callout>

Webhook payload format and signature verification: [Webhooks](/wiki/webhooks).

---

## Next steps

- **API** → [API References](/api) for endpoint specs.
- **Widget** → [Widget Integration](/integration/widget) for the embed snippet.
- **Iframe** → [Iframe Integration](/integration/iframe-integration) for the no-code path.
- Need a call? [Reserve an integration meeting slot](https://calendar.app.google/iezq9wfH1oxTbHMG8).
