Web SDKPayments

Payments

Which mode applies to you is fixed by partner.mor_mode in your session, and mirrored onto the element’s mor-mode attribute. It is a commercial setting, not something the page chooses.

mor_modeWho takes the money
drivecarsDriveCars. The widget renders a card form and charges the customer.
partner_invoicedYou. The widget collects no card details at all.

drivecars — DriveCars is merchant of record

The default, and the simpler one. Checkout renders a card form inside the widget, the customer pays DriveCars, and the widget authorizes the booking itself. The card form’s publishable key arrives with the payment intent, so you pass no payment credentials at all — there is nothing of yours to configure, and nothing of yours that could leak.

You need to do exactly one thing: listen for booking.payment.authorized and show your own confirmation.

<drivecars-checkout base-url="https://{your-base-url}"></drivecars-checkout>

Nothing else is required of you. The booking is confirmed by the time that event fires.

partner_invoiced — you are merchant of record

The widget never touches card details, and nothing the customer’s browser can do marks the booking paid. The sequence is:

  1. Checkout creates the draft booking and emits bookingReady with { bookingReference }.
  2. You charge the customer through your own PSP, however you like.
  3. Once your PSP authorizes, your server confirms the payment to DriveCars with POST /v1/bookings/{reference}/payment.
  4. The widget, which has been watching the booking, advances to its terminal screen and emits booking.payment.authorized. You also receive the booking.payment_authorized webhook.
  5. When a crew has been assigned, you receive booking.confirmed — the booking is now fulfilable. Every state change after that (cancelled, completed) arrives the same way.

Until step 3 lands, the booking is a draft: not confirmed, not fulfilled, not invoiced. It expires like any other draft if you never confirm — which is the safe outcome.

You do not have to rely on webhooks alone: GET /v1/bookings/{reference} returns the booking’s current status and payment_status at any time, and Reading and cancelling bookings covers the shape. Poll it if your webhook endpoint is not ready yet, or to reconcile.

// In the browser: learn the reference, then hand off to your backend.
document.addEventListener('bookingReady', async (e) => {
  const { bookingReference } = e.detail;
  await fetch('/api/pay-drivecars-booking', {
    method: 'POST',
    body: JSON.stringify({ bookingReference }),
  });
});
# On your server, once your PSP has authorized the charge.
# Scope: rentals:book (or transfers:book). Idempotent for the same reference.
curl -sS -X POST "https://{your-base-url}/v1/bookings/DC-8F2K91/payment" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "payment_reference": "ch_3P8k2Q…" }'
{
  "ok": true,
  "reference": "DC-8F2K91",
  "status": "pending",
  "payment_status": "authorized",
  "payment_reference": "ch_3P8k2Q…",
  "already_confirmed": false
}

payment_reference is your PSP’s reference for the charge. It is stored for reconciliation and shown on your invoice, and never sent to any card processor. Confirming the same booking twice with the same reference answers 200 again; with a different reference it is 409 payment_reference_mismatch, because two references for one charge is something you want to hear about now, not at invoice time. 409 not_partner_invoiced means DriveCars took the payment for that booking and there is nothing for you to confirm.

Do not confirm before your PSP has actually authorized. A booking that is never confirmed simply stays a draft; confirming a charge that did not succeed is not recoverable from your side.

confirmPartnerPayment(reference, paymentReference) — optional

This browser-side call is now purely for reconciliation: it records your PSP reference against the booking right away, so it is on the row even if your server’s confirmation is delayed. It does not confirm payment and does not advance the widget — the widget advances only when your server’s call has landed. You can skip it entirely.

The first argument selects which mounted <drivecars-checkout> to call, matched against the reference you passed to mount('checkout', el, { reference }). With exactly one checkout mounted the first argument is ignored; with more than one it must match a mount() label.

A payment link collects money for a booking that already exists, from a person who may have no account and no open session — a higher price after a crew’s counter-offer, a top-up, a deposit, a damage charge. It is a signed, single-use token that names one booking, one amount, one currency and one reason, and expires (7 days by default, 7 days at most). Whoever holds the token can view and pay that one amount; nothing else.

Its lifecycle is pending → paid, or pending → expired when nobody pays in time. There is no cancel or decline call on a link: an unwanted one is simply left to expire.

Before you can use one

  • DriveCars must be merchant of record for the booking. A link is paid through our card processor; under partner_invoiced you collect money yourself and confirm it from your server instead.
  • An access token from POST /oauth/token, exactly as for /v1 — see Authentication. Any booking scope works; the booking must be yours (another partner’s reference is a 404, never a 403 — see why).
  • Somewhere for the customer to pay: either the hosted page we return with every link, or your own page with <drivecars-payment> mounted (mount('payment', …) or the bare element).

Where the token comes from

There are three sources. In every case the token is minted by our API and stored against the booking; nothing about it is ever computed on your side.

1. You mint it. POST /bookings/{reference}/payment-link lives at the root of your base URL beside /oauth/token — not under /v1, which is why it is absent from the API Reference.

curl -sS -X POST "https://{your-base-url}/bookings/DC-2026-EWAG94/payment-link" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 120, "currency": "EUR", "reason": "damage_charge", "ttlHours": 72 }'
{
  "token": "eyJhbGciOi...",
  "url": "https://drivecars.ai/pay?t=eyJhbGciOi...",
  "expiresAt": "2026-09-20T21:00:00.000Z"
}
FieldNotes
amountMajor units (120 is €120.00), must be positive. Required unless checkoutHold is set.
currencyISO 4217, three letters. Required unless checkoutHold is set.
reasonOne of revised_price, top_up, deposit, damage_charge. Default top_up. Only revised_price changes the booking’s state when paid — see below.
ttlHours1–168. Default 168 (7 days).
checkoutHoldtrue makes the link be the checkout payment: we compute the amount ourselves (the held share of a rental, the full price of a transfer) in the booking’s locked currency, so amount and currency are ignored. For a booking whose checkout has not been paid.

Failures: 401 (missing_partner_token, invalid_partner_token), 404 (no such booking, or not yours), 400 when amount/currency are missing without checkoutHold, 409 amount_unavailable when checkoutHold finds no positive total.

2. We mint it for you, on a revised price. When the crew that takes a booking comes back with a price above the one the customer accepted, the platform mints a revised_price link itself and sends the customer the hosted URL by email and SMS. You do nothing here; you learn the outcome through webhooks (booking.confirmed) or by polling the booking. This variant does not take a new card: the customer consents to the new total with a one-time code sent to their phone, and the funds already held at checkout are captured at the new amount.

3. DriveCars ops mint one by hand — an extra charge on a confirmed booking, agreed with you or the customer. Same token, same page.

Paying it

  • Hosted: send the customer the url we returned. It opens our own pay page, which needs nothing from you.
  • Embedded: put the token on your page.
<drivecars-payment
  base-url="https://{your-base-url}"
  payment-link-token="eyJhbGciOi...">
</drivecars-payment>

The element makes three calls, each authenticated by the token in its path and nothing else: it reads the link (GET /payment-links/{token} — amount, currency, reason and a booking summary for the receipt), opens a card payment (POST /payment-links/{token}/intent — the card form’s publishable key arrives here, you pass none), then confirms it (POST /payment-links/{token}/confirm). It does not use the session token, so it is the one element that never asks for a session refresh and never emits tokenRefreshNeeded. When the payment finishes it emits payment.completed with { reference, kind } — see Events. An expired, revoked, already-paid or malformed token renders an error state instead of a form; without the attribute at all it renders a red Missing payment-link-token attribute.

What paying it does to the booking

  • The link becomes paid; paying it twice is a no-op, not a second charge.
  • revised_price links confirm the booking — you receive booking.confirmed, and GET /v1/bookings/{reference} reports confirmed. See Reading bookings.
  • top_up, deposit and damage_charge links record the charge against the booking and change nothing else about its state.

Keep the token private

Because the link token is the credential, treat the URL you put it in as sensitive: anyone holding it can view and pay that booking. Send it to the customer directly, and do not log it or put it in a referrer-leaking query string on a page with third-party scripts. The hosted URL carries it as a query parameter (?t=) for exactly that reason — a token in the path would be cached and logged as a page.