Overview
- Live prices. Agreed route prices, distance-based fares, demand and last-minute rules, plus your agreed net rates or adjustment. No static price list.
- Live availability. The same sales rules as our website: real seat and luggage capacity, minimum notice by area, vehicle class and distance, service area.
- Real bookings. A QuickPickups booking number, a professional driver through our dispatch, flight tracking and a live-tracking page for the passenger.
- Real-time status. Driver name, vehicle and plate, on the way, arrived, on board, completed; GPS position from one hour before pickup.
- A full sandbox. Same endpoints, same webhooks, no real driver and no invoice.
Base URL for live and sandbox: https://backend.quickpickups.es/api/partners/v1. The key selects the environment. All amounts are EUR with VAT included; times are ISO 8601 (without offset they are local time in Barcelona).
Coverage
Use these place codes or send coordinates with an address. Read vehicle classes, capacities, child-seat types and notice rules from GET /coverage: they change with our fleet and are never hard-coded.
| Code | Place |
|---|---|
| BCN | Barcelona-El Prat Airport (add "terminal": "T1" or "T2") |
| GRO | Girona-Costa Brava Airport |
| REU | Reus Airport |
| BCN_PORT | Port of Barcelona (cruise terminals) |
| SANTS | Barcelona Sants railway station |
| FIRA_GRAN_VIA | Fira Barcelona Gran Via |
| BCN_CENTER | Barcelona city centre (Plaça de Catalunya) |
curl https://backend.quickpickups.es/api/partners/v1/coverageAuthentication and security
Send your key as Authorization: Bearer <key>. Sandbox keys start with qp_test_, live keys with qp_live_. Keys are stored hashed and shown once; we rotate them with a grace period so you can switch without downtime. Keys can be limited by scope (quotes, bookings:read, bookings:write, tracking) and by IP allowlist.
curl https://backend.quickpickups.es/api/partners/v1/me \
-H "Authorization: Bearer $QP_KEY"Rate limits. 60 requests per minute per key by default. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; above the limit you get 429 with Retry-After. Every response has an X-Request-Id.
Quotes
POST/quotes
One option per vehicle class that can be sold for the request, with your price, locked until expires_at (30 minutes by default). For airport arrivals send the scheduled landing time: we track the flight.
curl -X POST https://backend.quickpickups.es/api/partners/v1/quotes \
-H "Authorization: Bearer $QP_KEY" -H "Content-Type: application/json" \
-d '{
"pickup": {"place": "BCN", "terminal": "T1"},
"dropoff": {"lat": 41.3870, "lng": 2.1701,
"address": "Plaça de Catalunya 1, 08002 Barcelona"},
"pickup_at": "2026-11-02T14:30:00+01:00",
"passengers": 3,
"luggage": 3,
"child_seats": [{"type_id": 1, "count": 1}]
}'{
"id": "qt_Jx8d2kq0Lw3mZp1a",
"object": "quote",
"status": "ok",
"expires_at": "2026-10-16T12:30:00+02:00",
"currency": "EUR",
"options": [
{
"option_id": "qt_Jx8d2kq0Lw3mZp1a.3",
"vehicle_class": {"id": 3, "code": "comfort", "name": "Comfort",
"max_passengers": 4, "max_luggage": 3,
"model_guaranteed": false},
"vehicles": 1,
"price_per_vehicle": …,
"price": …,
"currency": "EUR",
"pricing_basis": "net_rate"
}
],
"unavailable": [
{"vehicle_class": {"id": 9, "name": "Minibus 16"},
"reason": "too_soon", "min_lead_hours": 48}
]
}status: request_quote means nothing can be sold online for this request; reason says why (group_size, out_of_area, too_soon, stop_sale, long_distance, unpriceable) and our team quotes it by hand.
Bookings
POST/bookings
Books a quote option at the locked price. The Idempotency-Key header is required: retries with the same key and body return the same booking and never create a duplicate.
curl -X POST https://backend.quickpickups.es/api/partners/v1/bookings \
-H "Authorization: Bearer $QP_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2f0e-6c3a-4d0e-9a51-0b7f2b8f9e21" \
-d '{
"quote_id": "qt_Jx8d2kq0Lw3mZp1a",
"option_id": "qt_Jx8d2kq0Lw3mZp1a.3",
"partner_reference": "YOUR-REF-102938",
"passenger": {"name": "Jane Doe", "phone": "+44 7700 900123"},
"flight_number": "BA478",
"notes": "Two large suitcases and a pushchair"
}'{
"id": "QP26K4M7PX",
"object": "booking",
"environment": "live",
"status": "confirmed",
"partner_reference": "YOUR-REF-102938",
"pickup_at": "2026-11-02T14:30:00+01:00",
"pickup": {"place": "BCN", "terminal": "T1", "lat": 41.2889, "lng": 2.0731,
"address": "Barcelona-El Prat Airport (BCN) - Terminal T1"},
"vehicles": 1,
"price": …,
"currency": "EUR",
"policies": {"free_waiting_minutes": 60, "meet_and_greet": true,
"flight_tracking": true},
"cancellation": {"cancellable": true,
"free_until": "2026-11-01T14:30:00+01:00",
"fee_pct": 0, "fee": 0},
"tracking_url": "https://..."
}GET/bookings/{id}
GET/bookings?partner_reference=&status=&pickup_from=&pickup_to=
Statuses
| Status | Meaning |
|---|---|
| confirmed | Booked and accepted. |
| driver_assigned | A driver accepted the service: name, phone and vehicle are available. |
| driver_on_the_way | The driver is heading to the pickup point. |
| driver_arrived | The driver is at the pickup point (arrivals hall with a name sign at airports). |
| passenger_on_board | The service has started. |
| completed | The service has finished. |
| cancelled | Cancelled by you (partner) or by us (operator). |
| no_show | The passenger did not show up within the free waiting time. |
Amendments
PATCH/bookings/{id}
Passenger, flight number, notes and your reference change until the service starts. Route, time, group, child seats and vehicle class are re-priced by the live engine: if the price changes you get 409 price_changed with the new price; resend with accept_price_change: true. These changes close 2 hours before pickup by default. The driver is notified automatically.
curl -X PATCH https://backend.quickpickups.es/api/partners/v1/bookings/QP26K4M7PX \
-H "Authorization: Bearer $QP_KEY" -H "Content-Type: application/json" \
-d '{"pickup_at": "2026-11-02T16:00:00+01:00", "flight_number": "BA480"}'
# 409 price_changed → resend with "accept_price_change": trueCancellation
POST/bookings/{id}/cancel
Free until cancellation.free_until (24 hours before pickup by default), then your agreed fee applies. Send dry_run: true to preview the fee. Prepaid balances are refunded instantly.
curl -X POST https://backend.quickpickups.es/api/partners/v1/bookings/QP26K4M7PX/cancel \
-H "Authorization: Bearer $QP_KEY" -H "Content-Type: application/json" \
-d '{"dry_run": true}'
{"dry_run": true, "fee": 0.0, "fee_pct": 0.0,
"free_until": "2026-11-01T14:30:00+01:00", "currency": "EUR"}Tracking
GET/bookings/{id}/tracking
Driver first name, phone and vehicle (make, model, colour, plate) once assigned; live GPS position and ETA from one hour before pickup until the end of the service; a passenger-to-driver chat link. Poll at most every 15 seconds and rely on webhooks for changes.
{
"id": "QP26K4M7PX",
"status": "driver_on_the_way",
"driver": {"first_name": "Marta", "phone": "+34 600 000 000",
"vehicle": {"make": "Toyota", "model": "Corolla Hybrid",
"colour": "White", "plate": "1234 ABC"}},
"location": {"lat": 41.305, "lng": 2.081,
"updated_at": "2026-11-02T14:12:03+01:00"},
"eta_minutes": 6,
"location_status": "live",
"passenger_chat_url": "https://..."
}Webhooks
We POST signed JSON events to your HTTPS webhook URL (one for live, one for sandbox). Answer any 2xx within 5 seconds; otherwise we retry after 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h and 24 h. Deduplicate by event id: an event can arrive twice or out of order.
| Event | When |
|---|---|
| booking.confirmed | The booking is created. |
| booking.updated | The booking was amended through the API. |
| driver.assigned | A driver accepted the service (again if the driver changes). |
| driver.on_the_way | The driver set off to the pickup point. |
| driver.arrived | The driver is at the pickup point. |
| passenger.on_board | The service has started. |
| booking.completed | The service has finished. |
| booking.cancelled | Cancelled by you or by us. |
| passenger.no_show | The passenger did not show up. |
| flight.delayed | The tracked flight is 15+ minutes late (again for each further 15 minutes). |
| ping | Test event from POST /webhooks/test. |
POST https://your-system.example.com/webhooks/quickpickups
QP-Signature: t=1792141200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
QP-Event-Id: evt_Qm3xB0c8Lr1s9TzKp2aWv
QP-Event-Type: driver.assigned
{
"id": "evt_Qm3xB0c8Lr1s9TzKp2aWv",
"type": "driver.assigned",
"created_at": "2026-11-02T13:05:44+01:00",
"environment": "live",
"data": {"booking": { "...": "same object as GET /bookings/{id}" }}
}Verify the signature. QP-Signature: t=<unix>,v1=<hex> where the hex is HMAC-SHA256 of "<t>.<raw body>" with your signing secret. Reject events older than 5 minutes.
import hashlib, hmac, time
def verify(secret: str, raw_body: bytes, header: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])const crypto = require("crypto");
function verify(secret, rawBody, header) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}POST/webhooks/test
Sends a signed ping event to your URL.
Sandbox
Sandbox keys use the same endpoints, the same live pricing engine and the same validations. Sandbox bookings are never dispatched to a driver, never invoiced and never charged, and sandbox and live data are fully separate.
POST/sandbox/bookings/{id}/advance
Moves a sandbox booking to the next status, or to to: driver_assigned, driver_on_the_way, driver_arrived, passenger_on_board, completed, no_show or flight_delayed. Each step sends the real signed webhook with a test driver and vehicle.
curl -X POST https://backend.quickpickups.es/api/partners/v1/sandbox/bookings/QP26K4M7PX/advance \
-H "Authorization: Bearer $QP_TEST_KEY" -H "Content-Type: application/json" \
-d '{"to": "driver_arrived"}'
# → webhooks driver.assigned, driver.on_the_way, driver.arrivedErrors
{
"error": {"code": "quote_expired",
"message": "The quote has expired. Request a new one."},
"request_id": "req_3f9a1c2b7d4e5f60"
}| HTTP | Codes | What to do |
|---|---|---|
| 400 | idempotency_key_required, invalid_json | Fix the request. |
| 401 | missing_api_key, invalid_api_key | Check the key; it may be revoked or expired. |
| 402 | insufficient_balance, credit_limit_exceeded | Top up your balance or contact us. |
| 403 | live_not_enabled, insufficient_scope, ip_not_allowed, environment_mismatch, not_sandbox | Use the right key or ask us to change the setting. |
| 404 | quote_not_found, booking_not_found | Check the id and the environment. |
| 409 | quote_expired, quote_already_used, price_mismatch, price_changed, already_cancelled, not_cancellable, not_amendable, amend_window_closed | Re-quote, confirm the new price or contact us. |
| 422 | validation_error, invalid_location, unknown_place, out_of_area, pickup_in_past, too_soon, stop_sale, invalid_option, invalid_passenger, idempotency_key_reused | Fix the request or choose another option. |
| 429 | rate_limited | Wait Retry-After seconds. |
| 5xx | internal_error, service_unavailable | Retry with the same Idempotency-Key. |
Service levels
| Target | |
|---|---|
| API availability | 99.5 % per month |
| POST /bookings | under 1 second (p95) |
| POST /quotes | under 3 seconds (p95) |
| Webhook after the event | under 60 seconds |
| Operations desk | 24/7 by phone and WhatsApp, +34 711 206 600 |
| Technical support | [email protected], reply within one business day |
Planned maintenance is announced at least 48 hours in advance. Your agreed terms (payment, cancellation, waiting time, amendment cut-off, quote validity, rate limit) are returned by GET /me.