Quickstart

Getting started

Mobile phlebotomy is the default channel — the patient picks an address, we route a phlebotomist. The in-venue channel is the sibling: the patient comes to a venue at a booked slot. Both share the same booking endpoint and the same webhook envelope.

How does mobile fulfillment work?

You submit a MOBILE booking; we route it to the right phlebotomist based on coverage and availability. You don't pick the phleb — we do.

You learn the assigned phleb's name + phone via the dispatch.assigned webhook. Everything else (the phleb's app, our dispatch logic, in-app chat) is invisible to you by design — we own the workforce.

Your job lifecycle is the 8 webhook events + the GET /bookings/:id polling endpoint. If you need to surface status to a patient, that's what to wire.

Requesting scopes

Every API key is minted with an explicit set of scopes. Request only what you need in your access email — Lola staff set them at mint time. There are six:

bookings:read
List and fetch your bookings (GET /bookings, GET /bookings/:id) and their sample state.
bookings:write
Create bookings, mint hosted-booking URLs, and cancel bookings (POST /bookings, POST /booking-url, POST /bookings/:id/cancel).
venues:read
Discover the venue network and bookable slots (GET /venues, /venues/:id, /venues/:id/availability, /venue-field-definitions).
invoices:read
Read weekly invoices and their line items for reconciliation.
refunds:read
Read refund records issued against your bookings.
disputes:read
Read chargeback / dispute records raised against your bookings.

Mobile booking — end to end

The default channel. Seven steps from access request to results-ready, each one optional after step 3 — you can build a minimum integration on steps 1–4 alone.

  1. Step 1

    Get API access

    Email developers@loladispatch.com with your company, use case, and expected volume. Lola staff issue API keys after a short intake — no self-serve signup today.

  2. Step 2

    Get your pj_live_… key

    Once your client org has the API tier enabled, mint a key from clients.loladispatch.com/developer-settings. The plaintext value is shown once; the server stores only a SHA-256 hash.

  3. Step 3

    Create a MOBILE booking (WEEKLY_INVOICE billing)

    One POST. The response carries the new Job's id; you'll receive the signed job.created webhook in milliseconds.

    curl
    curl -X POST https://app.loladispatch.com/api/integration/v1/bookings \
      -H "X-API-Key: $LOLA_API_KEY" \
      -H "X-API-Version: 1" \
      -H "Content-Type: application/json" \
      -d '{
        "fulfillmentChannel": "MOBILE",
        "billingMode": "WEEKLY_INVOICE",
        "patient": {
          "firstName": "Jane",
          "lastName": "Doe",
          "email": "jane@example.com",
          "phone": "+447911123456",
          "address": "12 Example St, London, SW1A 1AA"
        },
        "scheduledFor": "2026-06-01T09:00:00Z",
        "testPanelIds": ["panel_core_health"],
        "externalUserId": "your-internal-user-id",
        "externalOrderId": "your-order-id"
      }'
    Node
    import { fetch } from 'undici';
    
    const res = await fetch('https://app.loladispatch.com/api/integration/v1/bookings', {
      method: 'POST',
      headers: {
        'X-API-Key': process.env.LOLA_API_KEY!,
        'X-API-Version': '1',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        fulfillmentChannel: 'MOBILE',
        billingMode: 'WEEKLY_INVOICE',
        patient: {
          firstName: 'Jane',
          lastName: 'Doe',
          email: 'jane@example.com',
          phone: '+447911123456',
          address: '12 Example St, London, SW1A 1AA',
        },
        scheduledFor: '2026-06-01T09:00:00Z',
        testPanelIds: ['panel_core_health'],
        externalUserId: 'your-internal-user-id',
        externalOrderId: 'your-order-id',
      }),
    });
    
    const job = await res.json();
    console.log(job.id, job.status); // CONFIRMED, paymentStatus PENDING
    Python
    import os
    import requests
    
    res = requests.post(
        "https://app.loladispatch.com/api/integration/v1/bookings",
        headers={
            "X-API-Key": os.environ["LOLA_API_KEY"],
            "X-API-Version": "1",
            "Content-Type": "application/json",
        },
        json={
            "fulfillmentChannel": "MOBILE",
            "billingMode": "WEEKLY_INVOICE",
            "patient": {
                "firstName": "Jane",
                "lastName": "Doe",
                "email": "jane@example.com",
                "phone": "+447911123456",
                "address": "12 Example St, London, SW1A 1AA",
            },
            "scheduledFor": "2026-06-01T09:00:00Z",
            "testPanelIds": ["panel_core_health"],
            "externalUserId": "your-internal-user-id",
            "externalOrderId": "your-order-id",
        },
        timeout=10,
    )
    res.raise_for_status()
    job = res.json()
    print(job["id"], job["status"])  # CONFIRMED, paymentStatus PENDING

    Request body

    JSON
    {
      "fulfillmentChannel": "MOBILE",
      "patient": {
        "firstName": "Jane",
        "lastName": "Doe",
        "email": "jane@example.com",
        "phone": "+447911123456",
        "dateOfBirth": "1990-04-12",
        "gender": "FEMALE"
      },
      "location": {
        "addressLine1": "12 Example Street",
        "city": "London",
        "postcode": "SW1A 1AA"
      },
      "appointment": {
        "preferredDate": "2026-06-01",
        "timePreference": "morning",
        "testType": "panel_core_health"
      },
      "externalUserId": "your-internal-user-id",
      "externalOrderId": "your-order-id"
    }

    Response (WEEKLY_INVOICE)

    PATIENT_PAY clients instead receive status: "PENDING_PAYMENT" and a paymentRequiredUrl the patient must open.

    JSON
    {
      "jobId": "ckxyz123",
      "status": "PENDING",
      "bookingUrl": "https://admin.loladispatch.com/jobs/ckxyz123"
    }
  4. Step 4

    Inspect the job.created envelope

    The first webhook lands on your endpoint. Your externalUserId and externalOrderId are echoed back on every event for the life of the Job — use them as your correlation keys, not Lola's internal jobId.

    JSON
    {
      "type": "job.created",
      "timestamp": "2026-06-01T08:14:02.123Z",
      "data": {
        "id": "evt_8c7b5d3a-2f4e-4d6a-9b1c-7e0a8d4f9c12",
        "jobId": "ckxyz...",
        "externalUserId": "your-internal-user-id",
        "externalOrderId": "your-order-id",
        "passthroughJson": null,
        "fulfillmentChannel": "MOBILE",
        "status": "CONFIRMED",
        "paymentStatus": "PENDING",
        "billingMode": "WEEKLY_INVOICE",
        "scheduledFor": "2026-06-01T09:00:00Z",
        "patient": {
          "firstName": "Jane",
          "lastName": "D.",
          "phoneLast4": "0000"
        }
      }
    }

    See Webhooks for signature verification + dedupe rules.

  5. Step 5

    Receive dispatch.assigned when a phleb accepts

    When a phlebotomist accepts the assignment, you receive dispatch.assigned with the phleb's name and phone number. This is the only event you need to surface the "who is coming" to your patient — Lola owns the rest of the dispatch UI.

  6. Step 6

    Receive job.in_progress when the phleb starts

    Use this as your "phleb arrived" proxy — see the honest-failure callouts for why there's no dedicated arrival event today.

  7. Step 7

    Receive job.completed + sample.registered

    When the phleb completes the visit and the sample is registered, both events fire. sample.registered carries the lab handoff metadata (shipmentMode, shipmentModeSource, clientMetadata, collectionNotes).

In-venue quick start

The sibling channel. The patient comes to a venue at a booked slot; venue staff check them in and collect the sample. Same booking endpoint, same webhook envelope — different inputs.

  1. Step 1

    List venues

    Filter by postcode + radius. Returns the venue id, name, address, hours, and whether it accepts public bookings.

    curl
    curl -X GET 'https://app.loladispatch.com/api/integration/v1/venues?postcode=SW1A&radiusKm=10' \
      -H "X-API-Key: $LOLA_API_KEY" \
      -H "X-API-Version: 1"
  2. Step 2

    Check slot availability

    Discrete slot starts within a date range — already capacity-checked against existing bookings. The slot start you pick is the one you POST in step 3.

    curl
    curl -X GET 'https://app.loladispatch.com/api/integration/v1/venues/venue_abc/availability?from=2026-06-01&to=2026-06-07' \
      -H "X-API-Key: $LOLA_API_KEY" \
      -H "X-API-Version: 1"
  3. Step 3

    Create an IN_VENUE booking

    Same endpoint as MOBILE; flip fulfillmentChannel to IN_VENUE and pass venueId + venueSlotStart instead of address + scheduledFor.

    curl
    curl -X POST https://app.loladispatch.com/api/integration/v1/bookings \
      -H "X-API-Key: $LOLA_API_KEY" \
      -H "X-API-Version: 1" \
      -H "Content-Type: application/json" \
      -d '{
        "fulfillmentChannel": "IN_VENUE",
        "billingMode": "WEEKLY_INVOICE",
        "venueId": "venue_abc",
        "venueSlotStart": "2026-06-02T10:30:00Z",
        "patient": {
          "firstName": "Jane",
          "lastName": "Doe",
          "email": "jane@example.com",
          "phone": "+447911123456"
        },
        "testPanelIds": ["panel_core_health"],
        "externalUserId": "your-internal-user-id",
        "externalOrderId": "your-order-id"
      }'
    Node
    import { fetch } from 'undici';
    
    const res = await fetch('https://app.loladispatch.com/api/integration/v1/bookings', {
      method: 'POST',
      headers: {
        'X-API-Key': process.env.LOLA_API_KEY!,
        'X-API-Version': '1',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        fulfillmentChannel: 'IN_VENUE',
        billingMode: 'WEEKLY_INVOICE',
        venueId: 'venue_abc',
        venueSlotStart: '2026-06-02T10:30:00Z',
        patient: {
          firstName: 'Jane',
          lastName: 'Doe',
          email: 'jane@example.com',
          phone: '+447911123456',
        },
        testPanelIds: ['panel_core_health'],
        externalUserId: 'your-internal-user-id',
        externalOrderId: 'your-order-id',
      }),
    });
    
    const job = await res.json();
    console.log(job.id, job.status); // CONFIRMED, paymentStatus PENDING
    Python
    import os
    import requests
    
    res = requests.post(
        "https://app.loladispatch.com/api/integration/v1/bookings",
        headers={
            "X-API-Key": os.environ["LOLA_API_KEY"],
            "X-API-Version": "1",
            "Content-Type": "application/json",
        },
        json={
            "fulfillmentChannel": "IN_VENUE",
            "billingMode": "WEEKLY_INVOICE",
            "venueId": "venue_abc",
            "venueSlotStart": "2026-06-02T10:30:00Z",
            "patient": {
                "firstName": "Jane",
                "lastName": "Doe",
                "email": "jane@example.com",
                "phone": "+447911123456",
            },
            "testPanelIds": ["panel_core_health"],
            "externalUserId": "your-internal-user-id",
            "externalOrderId": "your-order-id",
        },
        timeout=10,
    )
    res.raise_for_status()
    job = res.json()
    print(job["id"], job["status"])  # CONFIRMED, paymentStatus PENDING
  4. Step 4

    (Optional) Read venueCompletionFields

    Each venue can require additional fields at sample collection (e.g. fasting confirmation, allergy flags). Fetch the definitions ahead of time if you want to surface them in your own UI:

    curl
    curl -X GET https://app.loladispatch.com/api/integration/v1/venue-field-definitions \
      -H "X-API-Key: $LOLA_API_KEY" \
      -H "X-API-Version: 1"
  5. Step 5

    Receive job.in_progress with checkedInAt

    When venue staff check the patient in, you receive job.in_progress with a checkedInAt timestamp on the data envelope. No dispatch.assigned event fires for IN_VENUE — venue staff own the Job, no phleb is dispatched.

  6. Step 6

    Receive sample.registered

    Carries clientMetadata (the values venue staff entered for the venue's custom fields), collectionNotes, shipmentMode, and shipmentModeSource. Treat this as the "ready for lab" signal.

Event taxonomy at a glance

For the full taxonomy, fire-conditions, and reserved-for-LV1 events, see Webhooks. Repeated here as a quick reference while you build.

EventChannelsStatusSinceFires on
Job
job.createdBOTHLIVEv1Booking row written.
job.confirmedMOBILELIVEv1Phleb accepts. IN_VENUE jobs enter CONFIRMED at payment finalization (finalizePaidBooking), not at check-in.
job.in_progressBOTHLIVEv1Phleb starts (MOBILE) / venue check-in writes Job.startedAt (IN_VENUE — also fires checkedInAt).
job.completedBOTHLIVEv1Step-completion writes Job.completedAt.
job.cancelledBOTHLIVE (admin-side) / DEFERRED (phleb-side)v1Admin cancel; phleb-side cancel fires no webhook today — see Honest failure modes below.
job.rescheduledBOTHLIVEv1confirmedDateTime change (MOBILE) or venueSlotStart change (IN_VENUE).
job.en_routeMOBILELIVEv1Phleb marks en route; Job.enRouteAt written.
job.arrivedMOBILELIVEv1Phleb marks arrived; Job.arrivedAt written.
job.eta_updatedMOBILELIVEv1Phleb updates estimated arrival; payload carries previousEta + newEta.
job.no_showBOTHLIVEv1Patient flagged as a no-show.
Dispatch
dispatch.assignedMOBILE onlyLIVEv1Phleb accepts; payload carries the phleb name + phone.
dispatch.searchingMOBILE onlyLIVEv1Dispatch begins searching for a phleb; carries searchRadiusMiles.
dispatch.unassignedMOBILE onlyLIVEv1A previously-assigned phleb is removed; carries the reason.
Sample
sample.registeredBOTHLIVEv1Sample row created at completion; carries lab-handoff metadata.
sample.in_labBOTHLIVEv1Lab confirms receipt (Sample.status → RECEIVED_BY_LAB).
sample.results_readyBOTHLIVEv1Lab publishes results (Sample.status → RESULTS_READY); carries resultsUrl.
Invoice
invoice.createdBOTHLIVEv1Weekly invoice minted for a WEEKLY_INVOICE client.
invoice.paidBOTHLIVEv1Stripe confirms the invoice charge (invoice.paid webhook).
invoice.payment_failedBOTHLIVEv1A scheduled invoice charge fails.
invoice.overdueBOTHLIVEv1An unpaid invoice passes its due date.
Refund
refund.createdBOTHLIVEv1A refund is initiated against a Job.
refund.succeededBOTHLIVEv1Stripe confirms the refund settled.
refund.failedBOTHLIVEv1A refund attempt fails out-of-band.
Dispute
dispute.openedBOTHLIVEv1A chargeback is opened (charge.dispute.created); blocks refunds.
dispute.closedBOTHLIVEv1A chargeback is resolved (won or lost).
Venue
venue.activatedIN_VENUELIVEv1A venue is flipped to active (isActive false → true).
venue.deactivatedIN_VENUELIVEv1A venue is flipped to inactive; no new bookings accepted.

Ready to start?

The API reference has every endpoint, every parameter, and a try-it panel that uses your real API key. Webhooks has the verification deep-dive.