# Billej > Lej en bil på seks måneder eller mere. Fast pris om måneden med forsikring, service og grøn ejerafgift — og depositum retur, når bilen kommer hjem. Se flåden og bestil online. ## What this site is Billej is **one Danish car-rental company renting out its own cars**. It is not a marketplace and has no supply side: there are no lister accounts, no categories and no third-party sellers. The company publishes its fleet, a customer browses it, picks a car and checks out. Facts an agent must not get wrong: - The conversion event is a CHECKOUT, not an inquiry. The customer pays on this site, and a rental contract is issued as a result. - Every rental runs for a MINIMUM OF SIX MONTHS. A per-day or per-night booking does not exist here; a term is measured in whole months. - One car is one physical vehicle, identified by its registration number. Availability is a status on the row, not a stock count — two customers can never be sold the same registration. - At checkout the customer pays the first month's rent, a refundable deposit and a one-off establishment fee on a single card charge. The deposit is CHARGED, not authorised, and refunded at settlement. Months 2…N are collected monthly off-session on the saved card. - Currency is DKK, written `4.995 kr/md` and `12.400 kr`. Every amount in the API and the UI is WHOLE KRONER INCLUDING 25 % moms. Only the Stripe boundary works in øre. - Dates are `YYYY-MM-DD` day keys resolved in `Europe/Copenhagen`, never instants. - The payment schedule is DERIVED from the booking's start date, term and monthly price. It is never stored; only real payment attempts are. - Languages are Danish (`da`, default) and English (`en`). Nothing else. ## Key pages - Front page: https://www.billej.dk/ - The fleet: https://www.billej.dk/biler - One car: https://www.billej.dk/biler/{slug} - Checkout: https://www.billej.dk/reserver/{slug} - Confirmation: https://www.billej.dk/kvittering/{id} - Pricing, deposit and what a month includes: https://www.billej.dk/priser - How it works: https://www.billej.dk/saadan-virker-det - Guides: https://www.billej.dk/blogs - FAQ: https://www.billej.dk/faq - About: https://www.billej.dk/om - Contact: https://www.billej.dk/kontakt - Terms: https://www.billej.dk/vilkaar - Privacy: https://www.billej.dk/privatliv English aliases (`/cars`, `/pricing`, `/how-it-works`, `/about`, `/contact`, `/terms`, `/privacy`) answer with a 308 to the Danish path. Link to the Danish path directly and save the redirect hop. ## Further reading - OpenAPI 3.1 spec (JSON): https://www.billej.dk/api/openapi.json - Human-readable API reference: https://www.billej.dk/to-humans.md - Interactive API docs: https://www.billej.dk/api/docs --- # Billej API > Browse the fleet, price a rental, and reserve a car. Billej lets its own cars on agreements of at least six months. ## Authentication Session-based via cookie `better-auth.session_token`. Sign in: POST /api/auth/sign-in/email { email, password } Endpoints marked [public] do not require authentication. ## Guides ### GET /api/blogs Retrieve a list of Guides [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of Guides ### GET /api/blogs/{id} Find a Guide by ID [public] Responses: 200: Guide object 404: Guide not found ## FAQs ### GET /api/faqs Retrieve a list of FAQs [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of FAQs ### GET /api/faqs/{id} Find a FAQ by ID [public] Responses: 200: FAQ object 404: FAQ not found ## Media ### GET /api/media Retrieve a list of Media [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of Media ### GET /api/media/{id} Find a Media by ID [public] Responses: 200: Media object 404: Media not found ## addresses ### GET /api/addresses Retrieve a list of addresses [auth required] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of addresses ### POST /api/addresses Create a new addresses [auth required] Parameters: depth (number, query, optional): locale (string, query, optional): Request body (JSON): customer: string (optional) — ID of the users title: string | null (optional) firstName: string | null (optional) lastName: string | null (optional) company: string | null (optional) addressLine1: string | null (optional) addressLine2: string | null (optional) city: string | null (optional) state: string | null (optional) postalCode: string | null (optional) country: string [enum: US, GB, CA, AU, AT, BE, BR, BG, CY, CZ, DK, EE, FI, FR, DE, GR, HK, HU, IN, IE, IT, JP, LV, LT, LU, MY, MT, MX, NL, NZ, NO, PL, PT, RO, SG, SK, SI, ES, SE, CH] (required) phone: string | null (optional) Responses: 201: addresses object ### GET /api/addresses/{id} Find a addresses by ID [auth required] Responses: 200: addresses object 404: addresses not found ### PATCH /api/addresses/{id} Update a addresses [auth required] Request body (JSON): customer: string (optional) — ID of the users title: string | null (optional) firstName: string | null (optional) lastName: string | null (optional) company: string | null (optional) addressLine1: string | null (optional) addressLine2: string | null (optional) city: string | null (optional) state: string | null (optional) postalCode: string | null (optional) country: string [enum: US, GB, CA, AU, AT, BE, BR, BG, CY, CZ, DK, EE, FI, FR, DE, GR, HK, HU, IN, IE, IT, JP, LV, LT, LU, MY, MT, MX, NL, NZ, NO, PL, PT, RO, SG, SK, SI, ES, SE, CH] (optional) phone: string | null (optional) Responses: 200: addresses object 404: addresses not found ### DELETE /api/addresses/{id} Delete a addresses [auth required] Responses: 200: addresses object 404: addresses not found ## variants ### GET /api/variants Retrieve a list of variants [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of variants ### GET /api/variants/{id} Find a variants by ID [public] Responses: 200: variants object 404: variants not found ## variantTypes ### GET /api/variantTypes Retrieve a list of variantTypes [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of variantTypes ### GET /api/variantTypes/{id} Find a variantTypes by ID [public] Responses: 200: variantTypes object 404: variantTypes not found ## variantOptions ### GET /api/variantOptions Retrieve a list of variantOptions [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of variantOptions ### GET /api/variantOptions/{id} Find a variantOptions by ID [public] Responses: 200: variantOptions object 404: variantOptions not found ## products ### GET /api/products Retrieve a list of products [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of products ### GET /api/products/{id} Find a products by ID [public] Responses: 200: products object 404: products not found ## carts ### GET /api/carts Retrieve a list of carts [auth required] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of carts ### POST /api/carts Create a new carts [auth required] Parameters: depth (number, query, optional): locale (string, query, optional): Request body (JSON): items: array | null (optional) secret: string | null (optional) customer: string (optional) — ID of the users purchasedAt: string | null (optional) status: string | null [enum: active, purchased, abandoned] (optional) subtotal: number | null (optional) currency: string | null [enum: USD] (optional) discountCode: string | null (optional) — Applied discount code for this cart Responses: 201: carts object ### GET /api/carts/{id} Find a carts by ID [auth required] Responses: 200: carts object 404: carts not found ### PATCH /api/carts/{id} Update a carts [auth required] Request body (JSON): items: array | null (optional) secret: string | null (optional) customer: string (optional) — ID of the users purchasedAt: string | null (optional) status: string | null [enum: active, purchased, abandoned] (optional) subtotal: number | null (optional) currency: string | null [enum: USD] (optional) discountCode: string | null (optional) — Applied discount code for this cart Responses: 200: carts object 404: carts not found ### DELETE /api/carts/{id} Delete a carts [auth required] Responses: 200: carts object 404: carts not found ## Orders ### GET /api/orders List the authenticated user's orders [auth required] Returns a paginated list of orders belonging to the authenticated user. Orders are matched by the internal user ID or the user's email and are sorted by creation date (newest first). Parameters: page (integer, query, optional): Page number for pagination (defaults to 1) limit (integer, query, optional): Number of orders per page (defaults to 10, max 50) Responses: 200: Paginated order list 401: Not authenticated 500: Internal server error ## orders ### GET /api/orders/{id} Find a orders by ID [auth required] Responses: 200: orders object 404: orders not found ## Contact ### POST /api/contact Submit contact form [public] Accepts a contact form submission and stores it in the Payload CMS `contact-form-submissions` collection. No authentication is required. Request body (JSON): name: string [min: 1, max: 150, pattern: ^[\p{L}\p{N}\s\-'.]+$] (required) — Full name of the person submitting the form (e.g. "Jane Doe") email: string [min: 5, max: 320, format: email] (required) — Contact email address (e.g. "jane@example.com") subject: string [min: 1, max: 200] (required) — Subject line for the contact message (e.g. "Partnership inquiry") message: string [min: 1, max: 5000] (required) — Body of the contact message (e.g. "Hi, I would love to discuss a potential partnership. Please let me know a good time to connect.") Responses: 201: Contact form submitted successfully 400: Validation error — missing or invalid fields 500: Internal server error Dashboard: Payload Admin > Contact Form Submissions ## Newsletter ### POST /api/newsletter Subscribe to newsletter [public] Subscribes an email address to the newsletter. The address is stored in the Payload CMS `newsletter-subscribers` collection and optionally synced to a Resend audience when `RESEND_API_KEY` and `RESEND_AUDIENCE_ID` are configured. Duplicate emails are silently ignored. Request body (JSON): email: string [min: 5, max: 320, format: email] (required) — Email address to subscribe to the newsletter (e.g. "subscriber@example.com") Responses: 200: Successfully subscribed (or already subscribed) 400: Validation error — invalid email address 500: Internal server error Dashboard: Payload Admin > Newsletter Subscribers ## Cart ### POST /api/cart/apply-discount Apply a discount code to a cart [auth required] Validates and applies a discount code to a basket of boosts and plans. The caller must own the cart (via session) or supply the cart secret. Amounts are DKK øre — 14900 is 149,00 kr. Request body (JSON): code: string [min: 1, max: 50] (required) — The discount code to apply to the cart (e.g. "SOMMER20") cartId: integer [min: 1, max: 2147483647] (required) — The ID of the cart to apply the discount to (e.g. 42) secret: string [min: 1, max: 255] (optional) — Cart secret for guest users who are not authenticated but own the cart (e.g. "a1b2c3d4-e5f6-7890-abcd-ef1234567890") Responses: 200: Discount applied successfully 400: Malformed request body, or a discount code that does not apply. The two are distinguishable: a rejected code carries `success: false`, a malformed body carries `details`. 403: Not authorised to modify this cart 404: Cart not found or already purchased 500: Internal server error Dashboard: Use from the basket or /checkout by entering a discount code and pressing Apply. ### POST /api/cart/remove-discount Remove a discount code from a cart [auth required] Removes any previously applied discount code from a basket of boosts and plans. The caller must own the cart (via session) or supply the cart secret. Request body (JSON): cartId: integer [min: 1, max: 2147483647] (required) — The ID of the cart to remove the discount from (e.g. 42) secret: string [min: 1, max: 255] (optional) — Cart secret for guest users who are not authenticated but own the cart (e.g. "a1b2c3d4-e5f6-7890-abcd-ef1234567890") Responses: 200: Discount removed successfully 400: Missing or invalid cart ID 403: Not authorised to modify this cart 404: Cart not found or already purchased 500: Internal server error Dashboard: Use from the basket or /checkout by clicking the remove button next to the applied code. ## Discounts ### POST /api/discount/validate Validate a discount code [public] Validates a discount code for a boost or plan purchase: it must exist, be active, sit inside its valid date range, be under its usage limits and meet the minimum order amount. Optionally calculates the discount when a subtotal is supplied. All amounts are DKK øre — 14900 is 149,00 kr. Rate limited to 10 requests per IP per minute. Request body (JSON): code: string [min: 1, max: 50] (required) — The discount code to validate (e.g. "VELKOMMEN10") customerEmail: string [min: 3, max: 320, format: email] (optional) — Customer email for per-customer usage limit checks (e.g. "mette@eksempel.dk") subtotal: integer [min: 0, max: 99999999] (optional) — Cart subtotal in DKK øre for the minimum-order check and discount calculation (e.g. 14900) Responses: 200: Validation result. Both valid and invalid codes return 200; check the `valid` field. 400: Missing or invalid request body 429: Rate limit exceeded (10 requests per minute per IP) 500: Internal server error Rate limit: 10 requests per 60s ## Payments ### POST /api/payment-amount Calculate final payment amount with optional discount [auth required] Retrieves the current amount of a Stripe PaymentIntent for a boost or plan purchase and optionally applies a discount code. Every amount is DKK øre — 14900 is 149,00 kr. The PaymentIntent must still be in the `requires_payment_method` status. For authenticated users, ownership is verified via the Stripe customer. For guests, the PaymentIntent ID acts as authorization. Rate limited to 20 requests per IP per minute. Request body (JSON): paymentIntentId: string [min: 1, max: 255] (required) — The Stripe PaymentIntent ID (e.g. "pi_3Oc0X2Abc123def456") discountCode: string [min: 1, max: 50] (optional) — Optional discount code to apply to the payment (e.g. "SOMMER20") Responses: 200: Payment amount calculated successfully 400: Missing paymentIntentId, invalid payment state, or discount error 403: PaymentIntent does not belong to the authenticated user 429: Rate limit exceeded (20 requests per minute per IP) 500: Internal server error Rate limit: 20 requests per 60s ## Documentation ### GET /api/openapi.json OpenAPI 3.1 specification (JSON) [public] Returns this API's full OpenAPI 3.1 specification as JSON. Use this to generate client SDKs, import into API tools (Postman, Insomnia), or power interactive documentation UIs. Responses: 200: OpenAPI 3.1 JSON specification ### GET /llms.txt LLM-friendly API reference (plain text) [public] Returns the full API reference as structured plain text optimized for LLM context windows. Use this to feed API documentation into AI assistants, chatbots, or code generators. Responses: 200: Plain-text API reference ### GET /to-humans.md Human-readable API reference (Markdown) [public] Returns the full API reference as a Markdown document with table of contents, request/response tables, cURL examples, and error reference. Suitable for rendering in documentation sites or reading directly. Responses: 200: Markdown API reference