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_mode | Who takes the money |
|---|---|
drivecars | DriveCars. The widget renders a card form and charges the customer. |
partner_invoiced | You. 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:
- Checkout creates the draft booking and emits
bookingReadywith{ bookingReference }. - You charge the customer through your own PSP, however you like.
- Once your PSP authorizes, your server confirms the payment to DriveCars
with
POST /v1/bookings/{reference}/payment. - The widget, which has been watching the booking, advances to its terminal
screen and emits
booking.payment.authorized. You also receive thebooking.payment_authorizedwebhook. - 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.
Payment links
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_invoicedyou 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 a404, never a403— 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"
}| Field | Notes |
|---|---|
amount | Major units (120 is €120.00), must be positive. Required unless checkoutHold is set. |
currency | ISO 4217, three letters. Required unless checkoutHold is set. |
reason | One of revised_price, top_up, deposit, damage_charge. Default top_up. Only revised_price changes the booking’s state when paid — see below. |
ttlHours | 1–168. Default 168 (7 days). |
checkoutHold | true 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
urlwe 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_pricelinks confirm the booking — you receivebooking.confirmed, andGET /v1/bookings/{reference}reportsconfirmed. See Reading bookings.top_up,depositanddamage_chargelinks 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.