YipiiYipii IoT Docs

Routes & runs API

Send your routes, stops, timetables, daily runs and passengers to Yipii from your own system, with one JSON call per batch, and get webhooks back when passengers use their links.

Share

If your routes live in another system, such as a school-transport planner, TransportPlus or a spreadsheet your office keeps, you can send them to Yipii instead of typing them in. What you send becomes the same routes, runs and passengers your team sees under Sharing → Routes. Passengers then get a live link to their bus.

There are three ways in, and they all lead to the same place:

WayBest for
Settings → Import (a CSV, an Excel file or a Google Sheet)A one-off load, or an office that keeps a sheet. A connected Google Sheet is checked every 30 minutes.
The rows API (this page)Your system pushes changes as they happen.
POST /routes/sync (older)Only moving a route number onto a different vehicle.

Before you start

You need:

  1. An API key. Create one in Settings → API Access (see API Keys). The key acts as the person who created it. That person must be an admin, fleet manager or office staff in the account, or every import call answers 403.
  2. Your account key. This is the short name in your dashboard's address, such as onpoint in iot.yipii.io/onpoint/….

Imports go to the integrations service:

https://integrations.yipii.io/api/v1/{account_key}/imports

Every request carries Authorization: Bearer YOUR_API_KEY.

Send a route with its stops

A route is identified by its route number. Send its stops nested inside it, in the order the vehicle reaches them:

curl -X POST "https://integrations.yipii.io/api/v1/{account_key}/imports/rows" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target": "routes",
    "rows": [
      {
        "route_number": "S12",
        "route_name": "Mosta school run",
        "vehicle": "BUS 014",
        "driver": "maria@example.com",
        "days": "Mon-Fri",
        "start_time": "07:00",
        "end_time": "08:10",
        "stops": [
          { "name": "Mosta Church", "lat": 35.9097, "lng": 14.4258, "time": "07:05" },
          { "name": "Ta'\'' Qali",   "lat": 35.8947, "lng": 14.4155, "time": "20" },
          { "name": "St Aloysius",  "lat": 35.9040, "lng": 14.4637, "time": "08:00", "kind": "drop-off" }
        ]
      }
    ]
  }'

The answer comes back once every row has been written:

{
  "id": 412,
  "target": "routes",
  "status": "done",
  "source": "api",
  "rows": 3,
  "stats": { "created": 3, "updated": 0, "skipped": 0, "failed": 0 },
  "errors": [],
  "created_ids": [881],
  "finished_at": "2026-10-04T15:06:24+00:00"
}

rows counts stops, because each stop is one row, just as it would be in a spreadsheet. created_ids holds the ids of the routes that are new.

Send the same route again and it is updated, not duplicated. Stops are edited in place, so passengers already booked on a stop stay on it. Sending a renamed first stop renames that stop. It does not create a new one.

Route fields

FieldRequiredNotes
route_numberyesYour identifier. It matches the route on every later call.
route_nameShown to passengers.
vehicleA plate or vehicle name already in your account. Vehicles are matched, never created.
driverThe name, email or phone of someone in your account.
daysMon-Fri, Weekdays, Mon, Wed & Fri, Tuesday to Thursday, Daily, Weekends.
start_time, end_time07:00, 7.15, 3:30 pm.
effective_fromThe date the route starts.
stops[]Each stop takes name, address, lat, lng, time and kind.
stops[].timeA clock time (07:20) or minutes from the start (15).
stops[].kindpick-up or drop-off. Leave it out for a stop that is both.

Send the day's runs

A run is one day's trip on a route. A route always has its timetable; send runs only for what is different on a given day, such as a notice, another vehicle, another driver or a cancellation:

{
  "target": "runs",
  "rows": [
    { "route_number": "S12", "date": "2026-10-06", "notice": "Gate B today" },
    { "route_number": "S12", "date": "2026-10-07", "vehicle": "BUS 022" },
    { "route_number": "S14", "date": "2026-10-06", "status": "cancelled" }
  ]
}

There is one run per route per date. Sending the same route and date again updates that run, which is also how you cancel one. Dates in the past are skipped. Dates may be written 2026-10-06 or 06/10/2026.

FieldRequiredNotes
route_numberyes
dateyes
start_time, end_timeDefaults to the route's own times.
vehicle, driverFor this day only.
noticeShown to passengers on the run's link.
statusscheduled (default) or cancelled.

Send passengers

{
  "target": "passengers",
  "rows": [
    {
      "external_id": "STU-2291",
      "route_number": "S12",
      "passenger_name": "Maria Borg",
      "contact_name": "Anna Borg",
      "contact_phone": "+356 9912 3456",
      "pickup_stop": "Mosta Church",
      "dropoff_stop": "St Aloysius"
    }
  ]
}

Stops are matched by name, ignoring capitals, against the route's stops. A passenger is matched by external_id first, then by name on that route, so a second call updates the passenger it matches.

An import never messages anyone. To send the new passengers their links, pass the job's created_ids to:

curl -X POST "https://api.yipii.io/api/{account_key}/public-tracking/passengers/send-welcome" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "passenger_ids": [5120, 5121, 5122] }'

The answer counts who was sent a link and who was not, and why: {"counts": {"sent": 2, "not_approved": 0, "opted_out": 0, "no_channel": 1}}.

Large batches

Up to 500 rows, counting each stop as a row, are written before the call answers (200). From 501 to 5,000 rows the call answers 202 straight away with status: "pending". Read the result from:

GET https://integrations.yipii.io/api/v1/{account_key}/imports/{id}

Poll it until status is done or failed. Each key may make 30 row calls a minute.

When something is wrong

AnswerMeaning
404The account key is not one this API key belongs to.
403The key's person is not an admin, fleet manager or office staff.
422The whole call was refused. The most common cause is a misspelt field, such as Unknown field route_numbr. Nothing was imported.
200 with entries in errorsSome rows were left out. Each entry gives the row (its place in your call, counting from 1, with each stop counting as a row) and the reason, such as No Route number or No route S99 — import the route first.

To see every import and its fields as the server has them, call GET /api/v1/{account_key}/imports/targets.

Webhooks

Yipii can call your server when something happens on a passenger's link. Register an address with:

curl -X POST "https://api.yipii.io/api/{account_key}/public-tracking/webhooks" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/yipii", "events": ["viewer_joined", "access_requested"] }'

The answer includes a secret once. Save it, because it cannot be read again.

EventSent when
viewer_joinedSomebody opens a link.
access_requestedSomebody asks for access to a private link.
access_approvedYour team approves a request.
form_submittedA passenger fills in a link's form.

Each call is a POST of JSON with two headers:

  • X-Yipii-Event names the event.
  • X-Yipii-Signature is sha256= followed by the HMAC-SHA256 of the raw body, signed with your secret.

Check the signature against the raw body before you parse it:

import crypto from 'node:crypto'
 
function isFromYipii(rawBody, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
  return header.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected))
}
import hmac, hashlib
 
def is_from_yipii(raw_body: bytes, header: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(header, expected)

Answer with any 2xx within 5 seconds. A delivery is attempted once and is not retried. Every attempt is logged with its response code at GET /api/{account_key}/public-tracking/webhooks/{id}/deliveries. To check your endpoint, call POST …/webhooks/{id}/test, which sends a test_ping event.

Webhooks for runs, such as a run starting, a vehicle reaching a stop or a run finishing, are not available yet. Until they are, read a route's runs from GET /api/{account_key}/public-tracking/routes/{id}/occurrences.

Was this page helpful?

On this page