openapi.yaml Hae tunnukset

SECU Dealer · API v1

Kehittäjädokumentaatio

Suomeksi lyhyesti. Rajapinnalla tilaat puhelutarkastuksen, tarkastuksia ja ostopalvelun suoraan omasta järjestelmästäsi ja saat valmiin auton tiedot (kunto, havainnot, kuvat, leimat, raportin linkki ja myyntiteksti) takaisin rakenteisena JSON-datana. Tämä dokumentaatio on englanniksi, koska koodi ja kentät ovat englanniksi. Tunnukset haet osoitteessa /app/. Hinnat ovat alv 0 ja maksu tulee jälkikäteen laskulla.

Koneluettava kuvaus: openapi.yaml. Jos dokumentaation ja OpenAPI-kuvauksen välillä on ristiriita, OpenAPI-kuvaus on ensisijainen.

Quickstart

  1. Apply for access. After approval, sign in to the portal (email link, no password) and create an API key under API keys. Create a test key first.
  2. Place a call-check order for one car (listing URL is enough):
curl https://secudeals.com/api/dealer/v1/orders \
  -H "Authorization: Bearer $SECU_API_KEY" \
  -H "Idempotency-Key: STOCK-1042-callcheck" \
  -H "Content-Type: application/json" \
  -d '{
    "service": "call_check",
    "car": { "listing_url": "https://www.example.com/ilmoitus/12345678" },
    "options": { "reserve_if_good": true, "max_price": 18500, "auto_continue": "inspection" },
    "external_reference": "STOCK-1042"
  }'
  1. Receive progress either by webhook or by polling GET /orders/{id} or GET /events.
  2. When the order is completed, fetch the car feed with GET /cars/{car_id} and write it into your inventory system.

Base URL and format

Base URLhttps://secudeals.com/api/dealer/v1
FormatJSON requests and responses, UTF-8. Send Content-Type: application/json on POST.
TimestampsISO 8601 in UTC with Z, e.g. 2026-10-01T09:30:00Z. Convert to local time (Europe/Helsinki) in your UI.
MoneyDecimal numbers in EUR. All prices are excl. VAT ("vat": "excluded"); invoices to Finnish companies add 25.5 % VAT, EU companies are invoiced under reverse charge.
PaginationList endpoints take ?limit= (1–100, default 25) and ?starting_after= (id of the last object you received). A list is {"object":"list","data":[…],"has_more":bool,"next_cursor":"…"|null}; pass next_cursor as starting_after to get the next page.
TracingEvery response has X-Request-Id and Dealer-API-Version. Quote the request id when you contact support. You may send your own X-Request-Id.
CORSAllowed (Bearer auth, no cookies). Do not call the API from a browser with a live key, because the key would be visible to anyone.

Authentication

Send your key as a Bearer token. Keys look like alk_live_… or alk_test_….

Authorization: Bearer alk_live_xxxxxxxxxxxxxxxxxxxxxxxx

Keys are shown in full only once, when created. Create one key per integration, and revoke a key in the portal if it leaks. A dealer sees only its own data. GET /pricing is public and needs no key.

Test mode

A alk_test_… key creates orders with "mode": "test" and ids prefixed DT-. Test orders are not worked by our staff, send no messages, accrue no billable charges, and progress automatically to completed with a sample report. Use test mode to build and verify your integration, including webhooks. Live and test data are separate.

Errors

Errors use HTTP status codes and a consistent body:

{
  "error": {
    "code": "invalid_car",
    "message": "car: anna listing_url, vin tai make+model",
    "details": { }
  }
}

code is stable and machine-readable; message is for humans and may be in Finnish. details is optional.

HTTPcodeMeaning
400invalid_json, invalid_idempotency_keyMalformed body or header.
401missing_api_key, invalid_api_keyNo key, or the key is invalid or revoked.
403account_inactive, dealer_inactiveThe dealer account is not active.
404not_foundUnknown endpoint, or the object is not yours (you never learn whether it exists).
409invalid_state, not_ready, decision_conflict, transport_conflict, idempotency_in_progressAction not possible in the current state, report not ready yet, a different decision/transport choice was already made, or an identical request is still running.
413payload_too_largeRequest body over 1 MB.
422invalid_car, invalid_service, invalid_options, invalid_parameter, invalid_decision, invalid_price, missing_details, idempotency_key_reusedValidation failed (invalid_*), or the Idempotency-Key was used with a different request.
429rate_limitedToo many requests. See Retry-After.
501not_availableThe feature is not available (yet).
5xxinternal_error, upstream_errorOur side. Retry with the same Idempotency-Key; quote X-Request-Id if it persists.

Codes may be added over time; handle unknown codes by their HTTP status. The full list per endpoint is in openapi.yaml.

Rate limits and idempotency

Rate limit: 60 requests per minute per key. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds); over the limit you get 429 with Retry-After. Keep polling modest; webhooks are cheaper.

Idempotency: send an Idempotency-Key header (1–90 printable ASCII characters) on POST requests. Retrying the same request with the same key returns the original response (header Idempotent-Replay: true) and never creates or bills a second order; the same key with a different body returns 422 idempotency_key_reused. Use a key derived from your own record, e.g. stock number plus service. Keys are remembered for 48 hours. Always send one when you retry after a timeout.

Orders

One order = one service for one car. Create it with POST /orders.

Services

serviceNamePrice (excl. VAT)
call_checkHistory check + call to the seller; can reserve the car in the call39 €
inspection_basicBasic inspection229 €
inspectionKuntotarkastus™279 €
inspection_fullLaaja tarkastus™389 €
purchaseOstopalvelu™ (purchase service)Dealer price shown with your API key (GET /pricing) and in the portal

Prices above are the current list prices. Your account may have its own prices; the source of truth is GET /pricing and the price object of each order. Success fee: a share of the negotiated discount (asking price minus final price), billed only when a lower price is achieved; your rate is returned by GET /pricing and GET /me with your API key. Prices are VAT 0 %; Finnish companies are invoiced with 25.5 % VAT added (EU companies: reverse charge). Special vehicle types (camper, sports, classic, heavy, machine) add 150 € to inspections.

Request body

{
  "service": "call_check",                  // required, see table above
  "car": {                                  // at least one of: listing_url, vin, make+model
    "listing_url": "https://…",
    "vin": "WVWZZZ3CZKE123456",
    "make": "Volkswagen", "model": "Passat", "variant": "2.0 TDI",
    "year": 2019, "km": 142000, "price": 18900, "fuel": "diesel",
    "country": "DE", "city": "Hamburg", "zip": "20095",
    "seller_name": "…", "seller_phone": "…", "seller_type": "dealer",   // optional
    "notes": "…"
  },
  "options": {
    "reserve_if_good": true,                // call_check only: reserve in the call if the car looks good
    "max_price": 18500,                     // ceiling for the reservation
    "reserve_days": 3,                      // 1–14
    "auto_continue": "inspection",          // call_check only: continue to this service if verdict is good
    "vehicle_type": "car",                  // car|camper|sports|classic|heavy|machine
    "showroom_photos": false,
    "sales_text": true,                     // generate a sales text (default true)
    "tone": "…", "notes": "…"
  },
  "external_reference": "STOCK-1042"        // your own id, echoed back
}

Order object

{
  "id": "DO-2610-K7TQ", "object": "order", "mode": "live", "livemode": true,
  "car_id": "9f2c…",
  "service": "call_check", "status": "in_progress", "phase": "seller_call",
  "external_reference": "STOCK-1042",
  "car": { … }, "options": { … }, "parent_order_id": null,
  "verdict": null, "summary": null,
  "price": { "currency": "EUR", "vat": "excluded", "net": 39.0, "lines": [ … ] },
  "created_at": "2026-10-01T09:30:00Z", "updated_at": "…", "completed_at": null
}

When available the order also contains reservation (reserved_at, valid_until, agreed_price), deal (asking_price, final_price), report (url, view_url, verification_code), highlights[] and sales_text. Purchase-chain fields: decision, decision_options[] (while a decision is awaited), documents[], transport, delivery and, on GET /orders/{id}, process (see Decisions, contract & delivery).

Status, phase and verdict

FieldValues
statusreceived → in_progress → completed. Side branches: on_hold (credit limit reached; waits for release), cancelled.
phaseFree-form progress label, e.g. queued, history_check, seller_call, inspection_scheduled, inspection_done, report_ready. Show it to people; do not build logic on a fixed list.
verdictrecommended, reserved, caution, not_recommended, unreachable (seller not reached). Set when the order completes.

Listing: GET /orders filters: status (comma-separated), service, verdict, car_id, external_reference, created_after, created_before (ISO 8601).

Cancelling: POST /orders/{id}/cancel. Free before work has started; once the order is in_progress, the service is billed.

Chained orders: with auto_continue, a successful call_check creates the follow-up order automatically (parent_order_id points to the first one; an order.continued event is emitted).

Decisions, purchase contract and delivery

Suomeksi: prosessi etenee viidessä vaiheessa. Liike tilaa puhelutarkastuksen (halutessaan «varaa heti jos puhelussa vaikuttaa hyvältä»), saa historiaraportin ja puhelun tiedot ja päättää: Etene tarkastukseen tai Hylkää. Tarkastusraportin jälkeen päätös on Saa ostaa ja tinkiä tai Hylkää. Kaupan jälkeen liike allekirjoittaa kauppakirjan sähköisesti, näkee tingityn hinnan ja valitsee kuljetuksen (tilaus meiltä tai hoitaa itse). Toimituksen jälkeen tulee palvelulasku. Sama toimii portaalissa napeilla ja rajapinnasta alla kuvatuilla kutsuilla.

  1. Order a call_check (optionally options.reserve_if_good).
  2. Decide when the order completes (order.completed; the order has decision_options: ["proceed","reject"]): POST /orders/{id}/decision with {"decision":"proceed","service":"inspection"} or {"decision":"reject"}. proceed creates the inspection order (parent_order_id, event order.continued).
  3. Skip the inspection: after the history call you can also send {"decision":"buy","max_price":18500} to go straight to purchase and negotiation (no condition report, no SECU condition stamp).
  4. Decide again after the inspection report: {"decision":"buy","target_price":17500,"max_price":18500,"note":"…"} creates the purchase order (Ostopalvelu™: our staff negotiate; the success fee applies to the discount) or {"decision":"reject"}. buy is allowed unless the verdict is not_recommended/unreachable (caution is allowed).
  5. Contract and transport. The purchase order gets documents[]: {type:"purchase_contract", status:"awaiting_signature", sign_url, signed_at} (event document.created); open sign_url to e-sign with mouse or touch, after which the status becomes signed (event document.signed; read back automatically within minutes). The negotiated price is in deal.final_price. Choose transport: GET /orders/{id}/transport-quote (our truck freight from the car's location to your address, VAT 0), then POST /orders/{id}/transport with {"mode":"abco"} (we transport), "bahnexpress_own" (your own BahnExpress credentials) or "own" (you handle it). Event order.transport_chosen; the order's transport holds {mode, at, freight_order_id?}.
  6. Delivery. When the car is delivered the order gets delivery: {at, source} and the event order.delivered; the service invoice follows (see GET /invoices).

Idempotent decisions. Repeating the same decision returns the same result and never creates a second order; a different decision afterwards is 409 decision_conflict. Decisions cannot be changed. Choosing transport is idempotent likewise (409 transport_conflict if a freight order is already placed; cancel it first). A decision is also recorded when a follow-up already exists from auto_continue.

Progress. GET /orders/{id} returns process: five steps (order, call_decision, inspection, deal, delivery) with state done | current | action | stopped | todo; action means the ball is in your court (a decision, a signature or the transport choice). process.pending names the decision awaited and the order it concerns.

curl -X POST https://secudeals.com/api/dealer/v1/orders/DO-2610-K7TQ/decision \
  -H "Authorization: Bearer $ALK_KEY" -H "Content-Type: application/json" \
  -d '{"decision":"proceed","service":"inspection"}'
# → 200 { …order with "decision":{"decision":"proceed","at":"…","next_order_id":"DO-2610-M3XA"}, "next_order": { …new inspection order… } }
Test mode. Decisions create sandbox follow-up orders that advance automatically. A purchase order in test mode gets a sample contract (sample: true, signing is simulated about two minutes after completion); choosing own transport is marked delivered after about two minutes, a transport order from us after about five. No real documents, messages or transports are created. Steer the outcome of the follow-up order by writing sandbox:recommended|caution|… in the decision note.

Cars: the data feed

car_id identifies a car in your archive: all orders for the same car (matched by VIN, otherwise by listing id) share it. GET /cars lists your archive; GET /cars/{car_id} returns the complete structured feed intended for your inventory or website system:

The feed never contains the seller's or any customer's personal data, internal costs or inspector identities. Photo URLs accept ?w= (width 64–2400) and ?stamp=kunto|historia (apply a SECU stamp to the image); ?showroom=1 requests the showroom-background version where it has been ordered. Fetch images with your key (Bearer) from your server and store them with your listing.

GET /cars accepts vin, q (free text), external_reference and verdict. The feed can be incomplete while an order is still running; fetch it when order.completed arrives. In test mode the data is a sample.

Exact field names and types are defined in openapi.yaml.

Stamps and sales text

GET /cars/{car_id}/stamps returns the stamps issued for the car (verification code, kinds, verify URL, image URLs); ?download=kunto|historia&w=600 returns the PNG itself. A stamp is issued only when the data behind it has been verified, never when the verdict is not_recommended or unreachable. Verification: https://secu.fi/t/<code>. Stamp images: https://secudeals.com/stamp/<code>-kunto.png and …-historia.png.

You may use a stamp only for the car it was issued for, you must not edit it, and you must stop using it if it is revoked. See the terms, section 7. A revoked stamp is no longer rendered and the verification page says so.

GET /cars/{car_id}/sales-text returns the generated Finnish sales text (text, title, bullets, link, generated_at); POST to the same path regenerates it (optional tone; 20 per hour). In test mode the sample text is returned. The text is written from verified data only. Review it before publishing.

Freight

POST /freight/quote and POST /freight/orders price and order transport. Providers: abco (our truck freight, excl. VAT, price and estimated duration) and, if you have stored your own credentials in the portal, bahnexpress_own (a quote and order placed with your BahnExpress account). Stored credentials are encrypted and are never returned by the API. Track with GET /freight/orders/{id} or the freight.status event. Parameters are in openapi.yaml.

Events

Every state change creates an event. Read them via webhook or poll: GET /events?after=evt_123 returns newer events oldest-first (store the last id you processed); starting_after browses backwards. Filter with type (exact, or order.*) and order_id. Event envelope:

{
  "id": "evt_1042", "object": "event", "api_version": "2026-10-01",
  "type": "order.completed", "created_at": "2026-10-01T11:02:00Z",
  "livemode": true, "order_id": "DO-2610-K7TQ", "car_id": "9f2c…",
  "data": { …order object… }
}
typeWhen
order.createdOrder accepted by the API
order.on_holdCredit limit reached; order waits for release
order.acceptedWork has started (in_progress)
order.updatedphase changed
order.reservedCar reserved in the call
report.readyReport link available
order.deal_recordedFinal price recorded (success fee calculated)
stamps.issuedSECU stamps issued for the car
adtext.readySales text generated
order.completedOrder finished; fetch the car feed
order.continuedFollow-up order created (auto_continue or your decision)
order.cancelledOrder cancelled
freight.statusFreight order status changed
order.decisionYou decided (proceed, buy or reject); data.decision holds the record
document.createdA document (purchase contract) is ready for your signature
document.signedThe purchase contract was e-signed
order.transport_chosenTransport chosen (data.transport)
order.deliveredThe car was delivered (data.delivery)

Webhooks

Register an HTTPS endpoint with POST /webhooks (or in the portal). The signing secret (whsec_…) is shown once in the response. Subscribe to ["*"], to exact types, or to prefixes like "order.*". The format follows the Standard Webhooks convention.

Headers

webhook-id:        evt_1042          // same id on every retry: de-duplicate on it
webhook-timestamp: 1759312920        // unix seconds
webhook-signature: v1,K5oZfzN…       // base64 HMAC-SHA256

Verifying the signature

The signed content is {webhook-id}.{webhook-timestamp}.{raw body}. The key is the base64-decoded part of the secret after whsec_. Reject requests whose timestamp differs more than 5 minutes from your clock. Compare in constant time, and use the raw request body, not a re-encoded one.

import crypto from 'node:crypto';

export function verify(rawBody, headers, secret) {
  const id = headers['webhook-id'], ts = headers['webhook-timestamp'];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest('base64');
  return headers['webhook-signature'].split(' ').some(s => {
    const [v, sig] = s.split(',');
    return v === 'v1' && sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  });
}

Delivery and retries

Endpoint reference

Summary only. Parameters, schemas and error codes: openapi.yaml.

EndpointDescription
GET/pricingCurrent price list (public, no key needed; with a key you see your own prices).
GET/meYour account, credit limit and exposure, key mode and rate limit.
POST/ordersCreate an order. Supports Idempotency-Key.
GET/ordersList orders (limit, starting_after).
GET/orders/{id}Retrieve an order.
POST/orders/{id}/cancelCancel an order.
POST/orders/{id}/decisionDecide: proceed (inspection), buy (purchase — after the inspection, or directly after the history call without an inspection) or reject. Idempotent.
GET/orders/{id}/transport-quoteDirect transport quote for a purchase order.
POST/orders/{id}/transportChoose transport: abco, bahnexpress_own or own.
GET/orders/{id}/reportReport for a completed order (409 not_ready before that).
GET/carsYour archive of checked cars.
GET/cars/{car_id}Complete data feed for a car.
GET/cars/{car_id}/photos/{n}Photo n; options w, stamp, showroom.
GET/cars/{car_id}/stampsStamps issued for the car.
GET/cars/{car_id}/sales-textGenerated sales text.
POST/cars/{car_id}/sales-textRegenerate the sales text.
POST/freight/quoteFreight quote.
POST/freight/ordersOrder freight.
GET/freight/orders/{id}Freight order status.
GET/webhooksList webhook endpoints.
GET/webhooks/{id}Retrieve an endpoint with recent deliveries.
POST/webhooksCreate a webhook endpoint (secret shown once).
DELETE/webhooks/{id}Delete a webhook endpoint.
POST/webhooks/{id}/testSend a test event now.
POST/webhooks/{id}/deliveries/{delivery_id}/redeliverRedeliver a past delivery.
GET/eventsEvent feed for polling.
GET/invoicesYour invoices, plus the not-yet-invoiced total.
Keep keys secret. Never put a live key in browser code, a public repository or a mobile app. Rotate a key immediately if it leaks. See terms, section 11.

Changelog

VersionNotes
2026-10-01First release. Decision endpoint (/orders/{id}/decision), purchase contract documents, transport choice (/transport-quote, /transport), process and the events order.decision, order.transport_chosen, order.delivered, document.created, document.signed.

Questions: info@auto-saksasta.fi · 010 320 8080.