Mobile API: moving from v3 to v5
What this page is
Version 5 of the Mobile API does everything version 3 does: the member's objects, bookings, flight log, balance and transactions, and the bookings the member makes. What has changed is how the app and the member are identified, the addresses, the form of the replies, the status codes and the names and types of the values. This page lists every difference an app notices, with what to change. The names of version 3's functions are used as headings so the page can be read function by function.
Version 3 keeps working until 2027-09-30, but it will not be developed or changed: new functions are only added to version 5. Every v3 reply has a Deprecation header, a Sunset header with the end date and a Link header pointing to this page. From 2027-10-01 every v3 call is answered with 410 Gone.
Version 5 is built on the same framework as the Main API v5, so an app that also uses the Main API meets the same rules there. It is described in full on the documentation page. The same description can be fetched as openapi.yaml (OpenAPI 3.0) and as a Postman collection.
The most common changes
Every app needs the changes in this list. The sections after it have the details, function by function.
- Get a client id from myWebLog for your app and register its redirect addresses (write to support@myweblog.se). An app with a server of its own also gets a key, through a link that works once; keep it out of source code repositories and out of app binaries that users can unpack. A native app without a server gets no key (a public client). They replace
app_token. - Log the member in once with OAuth 2.1, the authorization code flow with PKCE: the app opens myWebLog's login page in the browser, the member logs in there, and the app exchanges the code at
POST /oauth/token. The app never sees the password, and your own login screen goes. Keep the access token and the refresh token; sendAuthorization: Bearer <access token>with every other call, and renew with the refresh token when a call answers 401. See Authentication. - Call one address per function with the HTTP method that fits, instead of
api_mobile.phpwithqtype:GET /bookings,POST /bookings,POST /bookings/{id}/canceland so on. See Where each function went. - Read the result from
data. One item is an object; a list is an array underdatawithmetaandlinksbeside it.Result,qTypeandAPIVersionare gone. - Fetch the organization once with
GET /organization. v3 sentorgDatawith every reply; v5 does not repeat it. - Read errors from the HTTP status code and a
Problem Detailsbody withcodeandtitle, instead ofError[{Code, Message}]with status 200. A refused booking rule is 409. - Read times as ISO 8601 text with a zone,
2026-10-04T08:00:00Zor2026-10-04T10:00:00+02:00, and send them the same way. v3 gave Unix timestamps for bookings and bare clock times for flights. - Read yes and no as
trueandfalse(v3:1and0), amounts and totals as numbers (v3: text such as"1.0000"), and member numbers as text. - Read a list page by page: follow
links.nextuntil it isnull. v3'slimitcut the list; v5'slimitandoffsetpage through it. - Send JSON in the body of a POST, with
Content-Type: application/json, except to the two/oauthendpoints, which take form data.returnType,charsetandlanguageare gone: replies are JSON in UTF-8, and the language of the booking messages comes from theAccept-Languageheader. - Remove parameters the endpoint does not have. v3 ignored them; v5 answers 400 and names them.
The rules that changed everywhere
Address and call
| v3 | v5 | What to do | |
|---|---|---|---|
| Address | https://api.myweblog.se/api_mobile.php, one address for everything | https://api.myweblog.se/mobile/v5/{resource}, one address per resource. No slash at the end: /me works, /me/ gives 404. | Change the base address and build the address per function. |
| The function | POST with the form field qtype | The HTTP method and the address: GET reads, POST creates or acts | See Where each function went. |
| The item | A form field: ac_id, bookingID, booking_id, transaction_id | In the address: /objects/710, /bookings/4711/cancel, /transactions/903 | Put the id in the address, never in the body. |
| The version | ?version=3.0.0, optional | In the address: /mobile/v5/ | Remove the parameter. |
| Parameters | Form fields in the POST body, also for reading | Query parameters for GET (/bookings?mine=true&date_from=2026-10-04); a JSON object in the body for POST | Send filters in the query string and data as JSON. |
| Reply format | returnType JSON, XML, ARRAY or PLIST; charset | JSON in UTF-8 only | Remove the two fields. Read JSON. |
| Language | Form field language (se, en, no) | The Accept-Language header: sv, en, nb or da. English when left out. | Send the header on the booking actions, whose messages are for the member. |
| Unknown parameters | Ignored | 400, naming the parameter or field | Remove misspelled and unused parameters. |
Authentication: OAuth 2.1
v3 identified the app with app_token and the member with mwl_u and mwl_p on every call, where mwl_p was the password or the user token from Login. v5 separates the two and uses OAuth 2.1 (the authorization code flow with PKCE), which every platform has a client library for. There is no password grant: no app ever handles a member's password.
| v3 | v5 | What to do | |
|---|---|---|---|
| The app | app_token with every call | A client id, sent only to the /oauth endpoints. An app with a server of its own also has a key, sent in a Basic Authorization header (client_id:key in base64) or as the form fields client_id and client_secret; a native app without a server has no key (a public client) and sends client_id alone. | Ask myWebLog for your client id (and key) and register your redirect address. Give them to your OAuth 2.0 library with the authorization URL https://api.myweblog.se/mobile/v5/oauth/authorize and the token URL https://api.myweblog.se/mobile/v5/oauth/token. |
| The member's login | mwl_u and mwl_p with every call; qtype=Login returned userToken, which could be sent as mwl_p from then on and never expired | The authorization code flow with PKCE: the app opens GET /oauth/authorize in the system browser, the member logs in on myWebLog's page and allows the app, and the app gets a code at its redirect address, which it exchanges at POST /oauth/token with grant_type=authorization_code, code, redirect_uri, code_verifier and, if you like, device_name. The reply has access_token (valid an hour), refresh_token (valid 90 days from its last use), expires_in and scope. | Remove your login screen: the app never handles the password. Use AppAuth or your platform's OAuth 2.0 library. Keep both tokens as you kept userToken. |
| Every other call | The credentials in the form fields | The header Authorization: Bearer <access token> | Send the header. When a call answers 401 with WWW-Authenticate: Bearer error="invalid_token", renew (below) and retry once. |
| Renewing | Not needed; the user token lived forever | POST /oauth/token with grant_type=refresh_token and refresh_token. The reply has a new access token and a new refresh token; the old refresh token is dead at once. | Renew when a call says so, not before every call. Replace both stored tokens. |
| Logging out | Forget the credentials | POST /oauth/revoke with the form field token = the refresh token. Always 200. | Call it at logout, then forget the tokens. The login disappears from the member's My settings on the website. |
| The member ends it | Not possible, except by a password change | The member sees the app's login on My settings on the website, next to device_name, and can end it. A password change ends every login. The next call then gets 401 and the refresh gives invalid_grant. | Treat invalid_grant on a refresh as "logged out": show the login screen. |
| Wrong password | Error 200 with status 200 | Handled on myWebLog's login page, which tells the member; the app never sees the password. The website's limits on failed logins apply there. If the member gives up, the app gets error=access_denied at its redirect address. | Nothing to show. Treat access_denied as "the member changed their mind" and show your start screen. |
| v3 user tokens | userToken from Login | Not accepted | Every member logs in again once, with their password, when the app moves to v5. |
| The app alone | Nothing | grant_type=client_credentials gives an access token for the app alone, good for GET /client only: does my key work, what are my rights. | Useful in a setup check; nothing else. |
Replies
| v3 | v5 | What to do | |
|---|---|---|---|
| The result | {"APIVersion": "3.0.0", "qType": "GetObjects", "result": {"orgData": ..., "Object": [...], "Result": true}} | {"data": ...}: one object for one item, an array for a list, with meta and links beside a list | Read data. Drop APIVersion, qType and Result. |
orgData | Sent with every reply | GET /organization, once per session. See orgData. | Fetch it after the login and keep it. |
| Empty values | A field that was empty was left out of the item (GetObjects), or was "" or "0" | Every field of an item is always there: null when there is no value, false for no, 0 for none | Stop testing whether a key exists. Test for null. |
| Numbers | Text: "1.0000", "123.45", "1118" | Numbers: 1, 123.45, 1118. Member numbers stay text, since they can have leading zeros. | Read numbers as numbers. |
| Yes and no | 1 and 0, or the key missing | true and false | Read and send booleans. |
| Points in time | Unix timestamps (bStart), clock times without a date (block_start), server-time text (created) | ISO 8601 with a zone: ..._at in UTC (2026-10-04T08:00:00Z), ..._local in the organization's time zone with its offset | Parse ISO 8601. Show _local, or convert _at with the organization's timezone. |
| Dates | yyyy-mm-dd | YYYY-MM-DD, the same | Nothing. |
| Names | Swedish and English mixed: Fornamn, bStart, platserkvar, nature_beskr | English, lower case with underscores, the same names as in the Main API v5 | See the tables below for each function. |
| Pictures | objectThumbnail as base64 text inside the object | GET /objects/{id}/thumbnail: the JPEG itself, with an ETag and a day's Cache-Control; has_thumbnail in the object says whether there is one | Fetch pictures as pictures, and cache them. |
Errors
| v3 | v5 | What to do | |
|---|---|---|---|
| Status code | 200 also for errors (403 for a blocked address) | The real status: 400, 401, 403, 404, 405, 409, 429, 500 | Test the status code first. |
| The body | {"APIVersion": "3.0.0", "Error": [{"Code": "200", "Message": "INVALID USERNAME OR PASSWORD"}]} | Problem Details (RFC 9457, content type application/problem+json): type, title, status, code, request_id, and errors when parameters, fields or booking rules are named | Read code and title. See Error codes for where each v3 code went. |
| A refused booking | Status 200 with errorMessage: the rule texts | 409 with code 10300 and the texts in errors[].message | Show the texts to the member, as before. |
| The OAuth endpoints | - | /oauth/token and /oauth/revoke answer in the OAuth form: error and error_description | Your OAuth library reads them. |
| Support | Nothing to quote | Every reply has a Request-Id header, also in the body of an error | Log it; quote it to support. |
Lists
| v3 | v5 | What to do | |
|---|---|---|---|
| Length | limit cut the list (transactions 20, flight log 50); the rest could not be fetched | limit (100, transactions 20; at most 500) and offset page through the whole list. meta.has_more and links.next say whether there is more. | Follow links.next until it is null. |
| Count | - | include_total=true adds meta.total | Only when the count is shown. |
| Fewer fields | - | exclude=field,field leaves fields out; include=block,block adds the optional blocks of an object | Ask for the object blocks you need; they are what takes time. |
Headers and limits
| v3 | v5 | What to do | |
|---|---|---|---|
| Calls per minute | No limit | 120 per member and 600 per app (over all its members). Every reply has RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; too many give 429 with Retry-After. | Fetch what a screen needs, not more; cache the organization and the objects. Wait Retry-After seconds on 429. |
| Content-Type of a POST | Form data | application/json, except the two /oauth endpoints (form data) | Send JSON. |
Request-Id | - | Optional in the request (your own id, at most 64 characters); always in the reply | Nothing. Quote it to support. |
| Blocked address | 403 with error 990 | 403 with code 10003 | Nothing. |
Function by function
Where each function went
v3 qtype |
v5 | Note |
|---|---|---|
Login | GET /oauth/authorize in the browser, then POST /oauth/token (grant_type=authorization_code), then GET /me | The member logs in on myWebLog's page; the tokens from the exchange, the member from GET /me. |
GetUserdata | GET /me | |
GetBalance | GET /me | The balance is in the member. |
orgData (with every reply) | GET /organization | Once per session. |
GetOrgSettings | GET /organization/settings | |
GetObjects | GET /objects, GET /objects/{id}, GET /objects/{id}/thumbnail | Blocks with include=. |
GetBookings | GET /bookings, GET /bookings/{id} | |
GetBookings with includeSun=1 | GET /sun-times | A resource of its own. |
CreateBooking | POST /bookings | 201 with the booking. |
CutBooking | POST /bookings/{id}/cut | |
DeleteBooking | POST /bookings/{id}/cancel | |
GetTransactions | GET /transactions, GET /transactions/{id} | |
GetFlightLog | GET /flightlogs, GET /flightlogs/{id} | |
GetFlightLogReversed | GET /flightlogs?order=desc | |
RegisterCapture | Not in version 5 | Write to support if your app uses it. |
| - | GET /client | New: the app's own rights and limits. |
Login, GetUserdata and GetBalance
The login gives tokens, not the member: fetch the member with GET /me after it. GetUserdata and GetBalance both became GET /me.
| v3 | v5 |
|---|---|
userData.userToken | The tokens of POST /oauth/token (see Authentication) |
userID / ID | id |
username | username |
medlemsnr | member_number (text) |
Fornamn / fornamn, Partikel / efternamn_partikel, Efternamn / efternamn, fullname / fullName | name.first, name.prefix, name.last, name.full |
user_category | user_category |
locked (1/0) | locked (true/false): the member may read but not book or change anything; such a call gives 403 with code 10302 |
hidden, lockedLogin | Removed. Such a member cannot log in: POST /oauth/token gives 400 invalid_grant with the reason in error_description. |
orgID / club_id, orgName, orgHomepage | organization.id, organization.name, organization.homepage |
Balance / saldo (text) | balance (number) |
currency_symbol, int_curr_symbol | currency.symbol, currency.code |
password (hashed, in GetUserdata) | Removed. |
orgData
v3 sent the organization with every reply as orgData. v5 gives it once, with GET /organization, with the fields an app needs and nothing else.
v3 orgData |
v5 GET /organization |
|---|---|
ID, name, homepage | id, name, homepage |
timeZone | location.timezone, and location.utc_offset (the offset right now) |
The reference airport (as sunData.refAirport) | location.reference_icao, location.latitude, location.longitude, location.country |
format_ymd | date_format |
clubLocale | locale |
currency_symbol, int_curr_symbol | currency.symbol, currency.code |
hide_booking | booking.enabled (the opposite). When false, only the booking endpoints refuse (403, code 10301); v3 refused every function with error 101. |
stdlength (text, hours) | booking.standard_length_hours (number) |
force_cnl_comment | booking.cancel_reason_required |
use_expected_airborne | booking.use_expected_airborne |
Everything else in orgData | Removed. Write to support if your app used a field that is not here. |
GetOrgSettings
GET /organization/settings. The same settings, those the organization has marked "show in the app" on the website, as a list under data instead of orgSettings. Each has id (the setting type, as before), value, value_type and the website's own fields for it.
GetObjects
GET /objects gives the same objects in the same order. v3 gave everything it had about every object, including the status, the remarks and the picture; v5 gives the basic fields and adds the rest only when asked, with include=. That is what makes the list fast.
| v3 | v5 |
|---|---|
Input ac_id (one object) | GET /objects/{id} |
Input includeObjectThumbnail=1, output objectThumbnail (base64) | GET /objects/{id}/thumbnail: the JPEG itself. has_thumbnail in the object says whether there is one. |
ObjectIds (a list of the ids beside the objects) | Removed. The ids are in the items. |
ID | id |
regnr | registration |
bobject_cat | category.id (0 Aircraft, 1 Equipment, 2 Premises) and category.name |
model | model.designator, and model.name (both null for equipment and premises) |
club_id, clubname | organization.id, organization.name |
comment | comment (null when empty; v3 left the key out) |
objectStatus | status.total_id, with include=status; also status.maintenance_id and status.remarks_id (0 OK, 1 yellow, 2 red) |
maintTimeDate.hoursToGoValue, hoursToGoText, flightStop_hoursToGoValue, flightStop_hoursToGoText | status.remaining_time.soft_airborne, soft_airborne_formatted, hard_airborne, hard_airborne_formatted, with include=status |
activeRemarks[]: remarkID, remarkDate, remarkBy, remarkText, remarkCategory | active_remarks[]: id, created_date, created_by, description, category_id, with include=active_remarks |
flightData.total: airborne, airborneText, block, blockText, tachtime, tachtimeText, landings | time_summary.total: airborne, airborne_formatted, block, block_formatted, tach, tach_formatted, landings, with include=time_summary |
ftData (flight-time totals per type) | Removed. Write to support if your app showed it. |
disclaimerHeadline, disclaimerText | disclaimer.headline, disclaimer.text (disclaimer is null when there is none), with include=disclaimer |
use_expected_airborne, force_expected_time (keys missing when 0) | settings.use_expected_airborne, settings.force_expected_time (true/false), and settings.max_booking_length, with include=settings |
GetBookings
GET /bookings gives what the booking calendar on the website shows the member, with the bookings in the queue last. The filters have new names; the sun times are a resource of their own.
| v3 | v5 |
|---|---|
Input ac_id | object_id |
Input mybookings=1 | mine=true |
Input from_date, to_date | date_from (today when left out, as before), date_to |
Input booking_id (one booking) | GET /bookings/{id} |
Input includeSun=1, output sunData | GET /sun-times?date_from=&date_to=: airport, timezone and days[] with date, dawn, sunrise, sunset and dusk, each in UTC (at) and local (local). At most 62 days. |
ID | id |
bStart, bEnd (Unix timestamps) | start_at, end_at (UTC, ISO 8601), start_local, end_local (the organization's zone, with offset) and timezone |
ac_id, regnr, bobject_cat, club_id | object.id, object.registration, object.category.id and .name, object.organization.id |
user_id, fullname, medlemsnr, email, completeMobile | user.id, user.name, user.member_number, user.email, user.mobile. Email and mobile are null when the member hides them on the website, as before. |
elevuserid | student: id, name, member_number, email, mobile, or null when the booking has no student |
typ | type (the same values, such as PRIV and SKOL) |
primary_booking | is_primary, and is_queue (its opposite) |
isOwnBooking, isStudent (1, or the key missing) | is_own, is_student (true/false) |
fritext | comment |
platserkvar | seats_left |
expected_airborne (text) | expected_airborne (number, hours) |
hide_details and the names left empty | details_hidden: true when the organization hides who has booked from other members; user and student then have null names and contact details |
thisUserCanEdit, thisUserCanDelete, thisUserCanCut, queueAllowed | permissions.can_edit, permissions.can_cancel, permissions.can_cut, permissions.queue_allowed |
| - | New: is_active_now, is_passed, created_at, created_by |
Everything else the row had (bStartLT, bClass, b_start, ...) | Removed. Write to support if your app used a field that is not here. |
CreateBooking
POST /bookings with a JSON body. The same rules and the same messages as before, since the booking is made by the website's booking class as in v3.
| v3 | v5 |
|---|---|
Input ac_id | object_id (number) |
Input bStart, bEnd: yyyy-mm-ddThh:mm+hh:mm, or a Unix timestamp | start_at, end_at: ISO 8601 with offset or Z, the seconds optional (2026-10-10T10:00+02:00, 2026-10-10T08:00:00Z). A Unix timestamp is not accepted. Both times are converted to the organization's zone alike (v3 did not convert the end time). |
Input fritext | comment |
Input expectedAirborne | expected_airborne (number, hours) |
Input platserkvar | seats_left (number) |
Input language | The Accept-Language header: sv, en, nb or da |
Output BookingID, Result | 201 with the booking as GET /bookings/{id} gives it under data, and a Location header with its address |
Output infoMessageTitle, infoMessage (the texts joined with blank lines) | messages[], each { "kind": "info", "text": "..." }. There is no title: show the texts. |
Output errorMessage (the rule texts, with status 200) | 409 with code 10300 and the rule texts in errors[].message |
| Error 300 "Invalid booking time" | 400 with code 10006 and the field named in errors |
| Error 201 for a locked member | 403 with code 10302 |
| Error 101 when the organization has switched booking off | 403 with code 10301 |
CutBooking and DeleteBooking
| v3 | v5 |
|---|---|
qtype=CutBooking, input bookingID | POST /bookings/{id}/cut, no body |
qtype=DeleteBooking, input bookingID, reason | POST /bookings/{id}/cancel, body { "reason": "..." } or {}. A reason is required when booking.cancel_reason_required on /organization is true, as before. |
Output infoMessageTitle, infoMessage, Result | 200 with the booking under data (after the cut as it is then; after the cancel as it was) and messages[] |
Output errorMessage with status 200 | 409 with code 10300 and the texts in errors; 404 when the booking is not one the member may see |
| Whether the member may | Known beforehand: permissions.can_cut and permissions.can_cancel on the booking |
GetTransactions
GET /transactions, newest first as before, 20 per page unless limit says otherwise, and a year back unless date_from says otherwise.
| v3 | v5 |
|---|---|
Input limit (cut the list at 20) | limit and offset page through the list; links.next gives the next page |
Input from_date, to_date | date_from, date_to |
Input transaction_id | GET /transactions/{id} |
Balance | meta.balance: the balance now, over all transactions |
InitialBalance | meta.opening_balance: the balance before the oldest transaction on the page |
BalanceAtToDate, UsedCountLimit | Removed (both were always empty). |
format_ymd, locale, currency_symbol, int_curr_symbol | On GET /organization: date_format, locale, currency.symbol, currency.code |
TransactionData[] | data[] |
ID | id |
date | date |
created (server time, yyyy-mm-dd hh:mm:ss) | created_at (UTC, ISO 8601) |
amount (text) | amount (number) |
comment | comment: a "#" and what follows it is left out, as before |
bookedby_fullname | booked_by |
balance (the balance after the transaction) | balance_after (number) |
GetFlightLog and GetFlightLogReversed
GET /flightlogs, oldest first; order=desc is what GetFlightLogReversed was. The fields are those of the Main API v5's flight log, so the clock times are points in time with a zone, not bare clock times, and the totals are numbers.
| v3 | v5 |
|---|---|
Input limit (cut the list at 50) | limit (100) and offset page through the list |
Input from_date, to_date | date_from, date_to |
Input myflights=1 | mine=true |
Input ac_id | object_id |
qtype=GetFlightLogReversed | order=desc |
FlightLog[] | data[] |
rowID | id |
flight_datum | date |
ac_id, regnr | object.id, object.registration, and object.organization |
user_id, fullname | pilot.id, pilot.name (and member_number, organization) |
instructor_user_id, instructor_fullname | instructor (the same fields), or null |
scout_user_id, scout_fullname | scout (the same fields), or null |
| The names shown whatever the organization's setting | crew_hidden: true when the organization hides the pilot in its log; pilot, instructor and scout are then null, as on the website |
departure, arrival (ICAO codes) | departure.airport.icao and .name, arrival.airport.icao and .name |
via | via_route |
block_start, block_end (hh:mm) | departure.blockoff_at, arrival.blockon_at (UTC, ISO 8601) |
airborne_start, airborne_end (hh:mm) | departure.takeoff_at, arrival.landing_at |
tach_start, tach_end | Removed. totals.tach keeps the tach time. |
block_total, airborne_total, tach_total (text, "1.0000") | totals.block, totals.airborne, totals.tach (numbers, hours) and totals.block_minutes, airborne_minutes, tach_minutes. A total is null when one of its two readings is missing. |
flights | number_of_flights |
nature_beskr | flight_type.name, and flight_type.id (flight_type is null when the flight has none) |
comment | comment |
distance | Removed. It is not kept for a flight. |
| - | New: is_solo_instruction (true for a student's solo flight, false with an instructor, null when not filled in) |
Error codes
v3 answered every error with status 200 and a list of Error items. v5 answers with the status code and one code. The full v5 table is on the documentation page.
| v3 | v5 |
|---|---|
0 SSL CONNECTION NOT DETECTED | 403, code 10000 |
100 INVALID CLUB OR APP TOKEN | 401 invalid_client from /oauth/token when the client id or key is wrong; 401, code 10001, from every other endpoint when the access token is missing, expired or no longer valid. 400 invalid_grant at the login when the app is not allowed for the member's organization. |
101 BOOKING DISABLED IN SETTINGS | 403, code 10301, from the booking endpoints only. Everything else works. |
200 INVALID USERNAME OR PASSWORD | Handled on myWebLog's login page: the member sees the message there and the app only gets a code, or error=access_denied if the member gives up. |
201 LOCKED OR INACTIVE | 400 invalid_grant at the login for a member who cannot log in; 403, code 10302, when a locked member tries to book, cut or cancel |
300 Invalid date format / Invalid booking time | 400, code 10005 (a query parameter) or 10006 (a body field), with the name in errors |
900 INVALID QTYPE | 404, code 10100 (no such address) or 405, code 10050 (the address exists, not with that method) |
990 IP BLACKLISTED | 403, code 10003 |
999 Incorrect version | Gone; the version is in the address |
| - | New: 400, code 10007 (an unknown parameter or field); 409, code 10300 (a booking rule); 429, code 10013 (too many calls); 403, code 10002 (the app lacks the right); 403, code 10305 (the app's own token on a member endpoint); 404, code 10304 (no picture) |
Help
Questions about moving to version 5 are answered by support@myweblog.se. Every reply from v5 has a Request-Id header; quote it when the question is about a certain call.