Main API: moving from v4 to v5
What this page is
Version 5 of the Main API does everything version 4 does. The functions are the same. What has changed is the addresses, the form of the replies, the status codes and the types of the values. This page lists every difference that a program calling the API will notice: what v4 does, what v5 does, and what to change.
Version 4 keeps working until 2027-09-30, but it will not be developed or changed: new functions are only added to version 5. Every v4 reply has a Deprecation header, a Sunset header with the end date and a Link header to this page. From 2027-10-01 every v4 call is answered with error 10060 (410 Gone). The same tokens work in both versions, so a program can be moved one call at a time.
New in version 5 is OAuth 2.0: the token is exchanged for an access token that is valid for an hour, instead of being sent with every call. Sending the token directly, as in v4, keeps working in v5 until 2027-12-31. See Authentication: OAuth 2.0.
Version 5 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 with every endpoint.
The most common changes
Most programs need the changes in this list. The sections after it have the details and the less common cases.
- Change the base address to
https://api.myweblog.se/main/v5/, and remove the slash at the end of every address:users/becomes/users. - Send the token as
Authorization: Bearer <token>. The word Bearer is required. Better: exchange the token for an access token with OAuth 2.0 (see Authentication), which is the only way accepted from 2028-01-01. - Put the id of an item in the address:
users/?id=5becomes/users/5. The same goes for PATCH and DELETE. - Read the result from
data. One item is an object, not a list, and an item that does not exist gives 404. - Replace
verbose=truewithinclude=and the names of the blocks that are needed. - Read yes and no as
trueandfalse(v4:1and0), and send filters the same way. - Read member numbers, phone numbers, postal codes and account numbers as text, and send them as text.
- Read a list page by page: follow
links.nextuntil it isnull. A page has 100 items unlesslimitsays otherwise (v4: 500). - Handle the status codes that are new or have moved: 403 (the token lacks the right), 404 (not found), 409 (cannot be carried out as things are) and 429 (too many calls).
- Read an error as one object with
codeandtitle, and witherrorswhen parameters or fields are wrong. - Remove parameters and fields that the endpoint does not have. v4 ignored them; v5 answers 400 and names them.
The rules that changed everywhere
Address and token
| v4 | v5 | What to do | |
|---|---|---|---|
| Base address | https://api.myweblog.se/main/v4/ | https://api.myweblog.se/main/v5/ | Change the base address. |
| Slash at the end | Every address ends with one: users/ | No slash: /users. /users/ gives 404, and the reply says why. | Remove the slash at the end. |
| Token | Created on the website | The same tokens work. No new token is needed, except for the sensitive user data (see Users). The website also shows each token's Client ID, used for OAuth 2.0. | Nothing. |
Authorization header | Bearer <token>, and the token alone is also accepted | Must be Bearer <access token> (OAuth 2.0), or Bearer <token> until 2027-12-31 | Add Bearer if it is missing, and move to OAuth 2.0 before 2028 (next section). |
Authentication: OAuth 2.0
v4 has one way to authenticate: the token from the website is sent with every call. v5 adds OAuth 2.0 client credentials, the standard way for a program to get access to an API. The token is exchanged for a short-lived access token, and the access token is what every call carries. The token itself travels to one address only, and seldom.
Until 2027-12-31 both ways work in v5, so a program can be moved to v5 first and to OAuth 2.0 later. Every reply to a call where the token is sent directly has a Sunset header with that date. From 2028-01-01 such a call is answered with 401 (code 10015). v4 is not affected by the date.
| Value | |
|---|---|
| Token URL | https://api.myweblog.se/main/v5/oauth/token |
| Grant type | client_credentials |
| Client ID | The number shown as "Client ID" next to the token on the website (Integrations, API) |
| Client secret | The token itself, the value that starts with mwla1_ |
| Client authentication | HTTP Basic (client_id:client_secret, the default in most libraries), or the form fields client_id and client_secret |
| Request body | Form data (application/x-www-form-urlencoded) with grant_type=client_credentials. Not JSON. |
| Access token lifetime | 3600 seconds. Read expires_in; do not assume the number. |
| Scopes | None to request. The rights set on the token on the website apply; the reply lists them in scope for information. |
| Rate limit, IP addresses, revocation | Those of the token. Fetching an access token counts as one call. Deleting the token on the website stops its access tokens at once. |
What a program does:
- Read the token's Client ID on the website and store it next to the token.
- Fetch an access token with one
POSTto the token URL. Keepaccess_tokenand the time it expires: now plusexpires_inseconds, minus a margin of about 60 seconds. - Where the program sent
Bearer <token>, sendBearer <access token>. Nothing else in the request changes. - On 401 with
error="invalid_token"in theWWW-Authenticateheader: forget the stored access token, fetch a new one, retry the request once. Any other 401 means the id or secret is wrong or the token was deleted; do not retry in a loop. - Fetch an access token when there is none or it is about to expire, not before every call.
With curl:
curl -u "4711:mwla1_your_token_here" \
-d "grant_type=client_credentials" \
"https://api.myweblog.se/main/v5/oauth/token"
## {"access_token":"mwlat_...","token_type":"Bearer","expires_in":3600,"scope":"users:rw transactions:r"}
## 2. Call the API with it
curl "https://api.myweblog.se/main/v5/token" \
-H "Authorization: Bearer mwlat_..."
In PHP:
$curl = curl_init('https://api.myweblog.se/main/v5/oauth/token');
curl_setopt($curl, CURLOPT_USERPWD, $client_id . ':' . $client_secret);
curl_setopt($curl, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$reply = json_decode(curl_exec($curl), true);
curl_close($curl);
if(empty($reply['access_token'])) return null;
// expires_in is seconds from now; keep a margin of 60 seconds
return ['token' => $reply['access_token'], 'expires_at' => time() + $reply['expires_in'] - 60];
}
// Before each call
if($access === null or $access['expires_at'] < time()){
$access = get_access_token($client_id, $client_secret);
}
$headers = ['Authorization: Bearer ' . $access['token']];
In Python with the requests package:
access = {"token": None, "expires_at": 0}
def bearer():
if access["expires_at"] < time.time():
r = requests.post("https://api.myweblog.se/main/v5/oauth/token", auth=(CLIENT_ID, CLIENT_SECRET), data={"grant_type": "client_credentials"})
r.raise_for_status()
access["token"] = r.json()["access_token"]
access["expires_at"] = time.time() + r.json()["expires_in"] - 60
return {"Authorization": "Bearer " + access["token"]}
r = requests.get("https://api.myweblog.se/main/v5/users", headers=bearer())
if r.status_code == 401 and "invalid_token" in r.headers.get("WWW-Authenticate", ""):
access["expires_at"] = 0
r = requests.get("https://api.myweblog.se/main/v5/users", headers=bearer())
Libraries that do the same with less code: requests-oauthlib (Python, BackendApplicationClient), IdentityModel (.NET), Spring Security's client registration (Java), league/oauth2-client (PHP). In Postman: Authorization type OAuth 2.0, grant type Client Credentials, the token URL above, client ID and client secret from the website, client authentication "Send as Basic Auth header", then Get New Access Token.
| Mistake | What you see | What to do |
|---|---|---|
| Fetching an access token before every call | It works, but every fetch counts against the 120 calls per minute | Keep the access token until expires_in minus a margin. |
| Sending the token request as JSON | 400 invalid_request | Send application/x-www-form-urlencoded, as the examples do. |
Keeping Bearer <token> in the calls after fetching an access token | It works until 2027-12-31, so the mistake hides. The replies have a Sunset header. | Send the fetched access_token. |
Retrying 401 invalid_client in a loop | 429 after a while, from the limit on failed attempts | Check the client id and the token; see whether the token was deleted on the website. |
| Calling from an IP address not in the token's list | 401 invalid_client at the token URL | The token's IP list applies to the token URL and to the calls alike. |
Replies
| v4 | v5 | What to do | |
|---|---|---|---|
| Result | Under the resource's name: {"users": [...]} | Always under data: {"data": ...} | Read data instead of the resource name. |
| One item | A list with one item, or an empty list | One object under data, or 404 | Read an object, and handle 404. |
| Token owner in every reply | auth_token block in every reply, also in errors | Not sent. GET /token has it. | Stop reading auth_token; call GET /token if it is needed. |
meta_information | {"objects": n} in every reply | A list has meta (see Lists). One item has none. | Read meta.count instead. |
| Content type of an error | application/json | application/problem+json | Accept both content types. |
| Error body | {"errors": [{"id", "message", "error_subcode"}]} | One error object: type, title, status, code, request_id, and errors (a list of field, code, message) when parameters or fields are wrong | Read code (the same numbers as v4's id) and title. error_subcode is gone. |
| Which field is wrong | Inside the message text | errors[].field | Use field. All problems are listed at once. |
| Missing values | null or "", as stored | Always null. The key is always there. | Treat null as "no value". |
| A list without items | [], null or the key left out | Always [] | Nothing. |
| Text that looks like a number | Turned into a number: member number 007 becomes 7 | Each field has one type. Member numbers, phone numbers, postal codes and account numbers are always text: "007". | Read these as text. |
| Yes/no values | 1 and 0 | true and false | Read booleans. |
| Points in time | Several forms: 2026-10-04T08:00:00+00:00, 2026-10-03 12:00:00 (server time) | Always UTC, written 2026-10-04T08:00:00Z, in a field whose name ends with _at | Read _at fields as UTC. |
| A reference to something else | Two fields: organization_id, organization_name | One object: organization: {"id", "name"} | Read .id and .name. |
Lists
| v4 | v5 | What to do | |
|---|---|---|---|
| Page size | limit, 500 if left out. Above 500 is cut to 500 without notice. | limit, 100 if left out, 1 to 500. Outside that gives 400. | Send limit=500 to keep v4's page size, or follow the pages. |
| Knowing if there is more | Not possible | meta.has_more, and the address of the next page in links.next | Follow links.next until it is null. |
| Order | Not fixed for every list (users had none) | Every list has a fixed order, the same as v4 where v4 had one | Nothing. |
| Total number | Not available | include_total=true adds meta.total | Ask for it on the first page only. |
| Extra data | verbose=true gives everything | include=block,block gives the named blocks | List the blocks that are needed. |
| Leaving fields out | Not available | exclude=field,field leaves the named fields out of each item, to make the reply smaller. It takes the fields an item always has, by their name at the top level of the item. Also where one item is returned, and on the reply to POST and PATCH. | Nothing. Use it where only a few fields are needed. |
| Yes/no filter | 0 = both, 1 = yes, 2 = no (also true/false) | true or false; leave the filter out for both. active=1 gives 400. | Change the values, and remove =0 filters. |
| Text search | * and _ are wildcards | * is the only wildcard. _ and % are ordinary characters. | Replace _ wildcards with *. |
| Dates in filters | YYYY-MM-DD and YYYYMMDD | YYYY-MM-DD only | Write dates with hyphens. |
A parameter without a value (?name=) | Treated as a filter on empty text, or ignored | 400 | Leave the parameter out. |
The id filter | ?id=5 on the list | Not a filter. The id is part of the address: /users/5. ?id=5 gives 400. | Use the address of the item. |
| Lists that had no paging (flight types, approved flight types) | Everything in one reply | Paged like every other list | Follow links.next, or send limit=500. |
Status codes
| Situation | v4 | v5 |
|---|---|---|
| Token missing or not accepted | 401 | 401 (now always and only this) |
| Access token expired (OAuth 2.0) | - | 401, with WWW-Authenticate: Bearer error="invalid_token": fetch a new one |
| Token sent directly after 2027-12-31 | Works | 401 (code 10015) |
| Token lacks the right for the module | 401 | 403 |
| A parameter has the wrong format | 401 | 400 |
| The item does not exist | 200 with an empty list (GET) | 404 |
| Unknown address | Apache's HTML page | 404 as JSON |
| Wrong method for the address | 405 | 405, with an Allow header |
| Too many calls | Not limited | 429, with Retry-After |
| Too many failed token checks from one IP address | 401 | 429, with Retry-After |
| Flight types for an organization that does not use the flight log | 500 | 409 (code 10200) |
| Approved flight types for a user that does not exist | 500 | 404 |
| Plain HTTP | 406 | Redirected to HTTPS by the server |
| A user or a transaction was created | 201 (user), 200 (transaction) | 201, with the address of the new item in the Location header |
| A user was deleted | 204 | 204 |
| The user to change or delete does not exist | 400 (code 10100) | 404 |
| A field of the body is missing or wrong | 400 | 400, with every wrong field in errors |
| PATCH with nothing to change | 304 | 400 (code 10105) |
| Username, member number or KSAK member number already in use | 400 (code 10030) | 409, with the fields in errors |
| The same transaction sent twice | 400 (code 10130) | 409 |
| Transaction dated before the lock date | 400 (code 10150) | 409 |
| Deleting an administrator, or a user with a balance | 400 (code 10105) | 409 |
| Changing name, address, email or phone of a user whose login was created elsewhere | 400 (code 10105) | 409 |
| Trial organization already has 5 active users | 403 (code 10040) | 409 |
| Login check with a wrong username or password | 401 | 200 with "valid": false |
| Login check for a username with too many failed logins | 429 (code 10012) | The same |
In v5, 403 only ever means that the token lacks a right, and 401 only that the token is not accepted. 409 means that the request is right in its form but cannot be carried out as things are.
Parameters, headers and limits
| v4 | v5 | What to do | |
|---|---|---|---|
| A query parameter the endpoint does not know | Ignored | 400, naming the parameter | Remove misspelled or unused parameters. |
| Body of a POST or PATCH that is not JSON | Treated as an empty body | 400 (code 10004) | Send a JSON object. |
Request-Id header | Returned as sent | Returned as sent (at most 64 printable characters). If none is sent, one is made and returned. It is also in the body of an error. | Nothing. Quote it to support. |
user_defined_key header | Deprecated | Removed | Use Request-Id. |
no_json_numeric_check header, no_autonum_json parameter | Development switches | Removed | Remove them. |
| Calls per minute | No limit | 120 per token and minute. Every reply has RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. | Slow down when RateLimit-Remaining is low; wait Retry-After seconds on 429. |
Sunset and Link headers | None | On every reply to a call where the token is sent directly: the date after which that stops working (2027-12-31) and a link to this guide. | Move to OAuth 2.0 before the date (see Authentication). |
Sending data (POST and PATCH)
| v4 | v5 | What to do | |
|---|---|---|---|
| The id of the item to change or delete | In the body (PATCH) or the query string (DELETE) | In the address: PATCH /users/5, DELETE /users/5. id in the body gives 400, and the reply says where it went. | Move the id to the address. |
| Types of values | Loose: "3" or 3, 1 or true, "12.50" or 12.5 | The type the value has in a reply. Text: member number, KSAK member number, phone numbers, account numbers ("007", not 7). Number: ids and amounts. true/false: yes or no. | Send each value with its type. A wrong type gives 400 that names the field. |
| A field the endpoint does not know | Ignored | 400 (code 10007), naming the field | Remove misspelled or unused fields. |
A single value where a list is expected ("email": "a@b.se") | Accepted | 400: email, user_groups and street_address must be lists | Send a list, also for one value. |
| Dates | YYYY-MM-DD and YYYYMMDD; for a transaction anything PHP could read as a date | YYYY-MM-DD only | Write dates with hyphens. |
| Errors in the body | Users: all listed, the field inside the message. Transactions: only the first one. | All listed at once in errors, each with field, code and message. A field inside an object is named address.city, an item of a list email[1] (counted from 0). | Read errors[].field. |
| The reply to POST and PATCH | The item under the resource name, as a list for transactions. PATCH returned everything verbose gives. | The item under data, as GET gives it. ?include= adds blocks, with the same rights as for GET. | Read data; ask for the blocks that are needed. |
| What is saved when creating or changing a user fails halfway | A change could be saved in part (the membership but not the login), and a 500 could come after the change was saved | Nothing: the login, the membership and the event log entry are saved together or not at all | Nothing. |
Error codes
New in v5:
| Code | Meaning |
|---|---|
| 10004 | The request body must be a JSON object |
| 10007 | Unknown parameter or field |
| 10013 | Too many requests |
| 10014 | Too many failed authorization attempts |
| 10015 | The token must be exchanged for an access token (401, from 2028-01-01) |
| 10106 | Other data depends on this: a setting was not deleted (409; only with the new expiry date types) |
| 10200 | The organization does not use the flight log (409) |
No longer sent:
| Code | In v4 | In v5 |
|---|---|---|
| 10003 | Invalid parameter data | Not used |
| 10120 | Invalid amount format | 10006, with amount in errors |
| 10140 | Comment can not be empty | 10006, with comment in errors ("Required") |
| 10160 | Invalid account format | 10006, with the account field in errors |
Kept, with a new status: 10030, 10040, 10105, 10130 and 10150 are now 409 (see Status codes). 10100 is 404 when it is the item in the address that does not exist.
Endpoint by endpoint
Where each endpoint went
| v4 | v5 | Note |
|---|---|---|
GET token/rights/ | GET /token | Returns description, owner, organization and rights. |
GET test/, POST test/ | GET /token | The way to check that a token works. |
GET organizations/location/ | GET /organization | Returns id, name and location. Any valid token, as in v4. |
GET users/ | GET /users | See Users. |
GET users/?id=5 | GET /users/5 | One object, or 404. |
GET users/memberships/ | GET /memberships, GET /memberships/3 | See Memberships and user groups. |
GET users/groups/ | GET /user-groups, GET /user-groups/7 | |
GET objects/ | GET /objects, GET /objects/24 | See Objects. |
GET objects/status/ | GET /objects?include=status | The status is a block of the object. |
GET bookings/ | GET /bookings, GET /bookings/4711 | See Bookings. |
GET flightlogs/ | GET /flightlogs, GET /flightlogs/88 | See Flight log. |
GET flightlogs/flighttypes/ | GET /flight-types | |
GET flightlogs/flighttypes/approved/?user_id=5 | GET /users/5/approved-flight-types | |
GET transactions/ | GET /transactions, GET /transactions/900 | See Transactions. |
POST users/ | POST /users | See Creating a user. |
PATCH users/ with the id in the body | PATCH /users/5 | See Changing a user. |
DELETE users/?id=5 | DELETE /users/5 | See Deleting a user. |
POST transactions/ | POST /transactions | See Creating a transaction. |
POST logincheck/ | POST /login-checks | See Login check. |
| Not in v4 | GET, POST /expiry-date-types, GET, PATCH, DELETE /expiry-date-types/12 | New in v5: the organization's expiry date types. A new module on the website, Organization settings (organization_settings). |
| Not in v4 | GET /users/5/expiry-dates, PUT, DELETE /users/5/expiry-dates/12, GET /expiry-dates | New in v5: a user's expiry dates, read, set and removed, and every member's dates in one list. The module Users (users). |
In GET /token, a module has the same name as its resource. A token's rights for Economy, Logbook, Login check and Organisation are listed as transactions, flightlogs, login_checks and organization (v4: economy, logbook, login_check, organisation).
Users
Fields of a user:
| v4 | v5 |
|---|---|
status.active (1/0) | active (true/false) |
status.locked, status.locked_login, status.has_invalid_email_address (only with verbose) | locked, locked_login, has_invalid_email_address, always there |
membership_id, membership_name | membership.id, membership.name (membership is null if the user has none) |
first_name, last_name_particle, last_name, last_first_name (only with verbose) | Always there |
ksak_member_number or nlf_member_number, a number, only present for organizations in Sweden or Norway | Both always there, as text, null when they do not apply |
organization (only with verbose) | Removed. It is always the token's own organization; GET /token and GET /organization have it. |
email[].is_invalid (1/0) | email[].invalid (true/false) |
roles left out when the user has none | roles is [] |
expiry_dates is null when there are none; is_expired | expiry_dates is []; expired is true, false, or null when there is no date |
annual_fee, member_account_balance, external_customer_number | account.annual_fee, account.balance, account.external_customer_number |
Blocks, replacing verbose=true. Each adds one key with the same name:
| Block | Contents | Right needed |
|---|---|---|
email | List of addresses with invalid and invalid_reason | users |
address | street_address (list), postal_code, city, country | users |
phone | home, work, mobile (country_code, initial_number, number) | users |
user_groups | List of id and name | users |
roles | List of admin, instructor, student, board_member, maintenance | users |
expiry_dates | List of id, name, expiry_date, expired | users |
account | balance, annual_fee, external_customer_number | users |
misc_info | Text | users |
personal_identification_number | Text | users and users_sensitive |
internal_notes | Text | users and users_sensitive |
emergency_contact | name, phone, relation | users and users_sensitive |
access_card | number, code, expiry_date | users and users_sensitive |
The four sensitive blocks need a token with the new right "users_sensitive". In v4, read access to Users was enough. A token cannot be changed, so an integration that needs these blocks needs a new token with that right.
Filters of GET /users:
| v4 | v5 |
|---|---|
id | Use GET /users/{id} |
name, username, member_number, membership_name (with * and _) | The same names, text search with * |
membership_id | The same |
active (0, 1, 2) | active (true, false) |
invalid_email | has_invalid_email_address (true, false) |
locked, locked_login (1, 0) | The same names (true, false) |
verbose | include |
Creating a user
POST /users. The field names are the same as in v4.
| v4 | v5 |
|---|---|
membership_id as 3 or "3" | A number: 3 |
user_groups as one id or a list, ids as numbers or text | A list of numbers: [7, 8] |
email as one address or a list | A list of at most two addresses |
address.street_address as one text or a list | A list of at most two rows |
ksak_member_number, phone.mobile.country_code, initial_number, number as numbers or text | Text with digits only: "070". The country code may still be written +46 or 0046. |
member_number as number or text | Text. "007" and "7" are different numbers. |
| No member number sent: the user gets the highest member number plus one | The same. If no user in the organization has a member number, the new one gets 10000 (v4: none). |
Reply 201 with the user under users | Reply 201 with the user under data, and its address in the Location header |
| Duplicate username, member number or KSAK member number: 400, the first found | 409, all of them in errors |
The username rule is unchanged: it starts with the organization's id and a dash, and has a name after it. The password is still not set through the API.
Changing a user
PATCH /users/{id}.
| v4 | v5 |
|---|---|
id in the body | In the address |
status: {"active": true} (also 1, "1", "true") | active: true or false |
email: [{"address": "a@b.se"}, {"address": "c@d.se"}], each place changed on its own | email: ["a@b.se", "c@d.se"]. The list replaces both addresses: send both to keep both, one to remove the second, [] to remove both. |
address.street_address: each row changed on its own | The list replaces both rows |
"" emptied a value; null did so for some fields only | null and "" both empty the value. first_name and last_name cannot be emptied. |
access_card.number longer than 50 characters and code longer than 10 were cut off without notice | 400 |
access_card.expiry_date as YYYY-MM-DD or YYYYMMDD | YYYY-MM-DD |
Reply: the user with everything verbose gives, including the sensitive data | The user as GET /users/{id} gives it. Blocks with ?include=; the sensitive ones need the right users_sensitive. |
Unchanged: what can be changed (names, active, email, street address, postal code, city, phone, misc_info, internal_notes, access card). Name, address, email and phone can only be changed for users whose login was created by the organization.
Deleting a user
DELETE /users/{id}. The same rules as in v4: not an administrator, and the balance must be zero. Those two now give 409 instead of 400, and a user that does not exist gives 404 instead of 400. The reply is 204 without a body, as in v4.
Memberships and user groups
| v4 | v5 |
|---|---|
id, name | The same |
verbose=true adds users | include=users adds users |
users[].active (1/0), users[].member_number as a number when it looks like one | active is true/false, member_number is text |
| Users of a membership or group in no fixed order | Sorted by name |
Filters id, name | name; the id is in the address |
Objects
Fields of an object:
| v4 | v5 |
|---|---|
organization_id, organization_name | organization.id, organization.name |
type_designator, type_name only present for aircraft | Always there, null for equipment and premises |
active (1/0) | active (true/false) |
verbose=true | Blocks: status, active_remarks, maintenance_items, settings, time_summary |
status.active_remarks | Its own block: active_remarks |
status.maintenance_items | Its own block: maintenance_items |
settings only present for aircraft; force_* as 1/0 | settings is null for equipment and premises; force_* as true/false; type_* as text |
objects/status/: id, registration, category_id, status_id | GET /objects?include=status: status.total_id is v4's status_id; the category is category.id |
Filter active (0, 1, 2) | active (true, false) |
objects/status/ in v4 listed the objects of the booking calendar. GET /objects lists all objects, as objects/ did; an object the website has no status for gets null in the status fields.
Bookings
| v4 | v5 |
|---|---|
times.start_utc, times.end_utc (...+00:00) | start_at, end_at (...Z) |
times.start_local, times.end_local | start_local, end_local |
object.timezone (as stored, may be empty) | timezone: the time zone the local times are given in (Europe/Stockholm when the organization has none) |
object.organization_id, object.organization_name | object.organization.id, object.organization.name |
object.category_id, object.category_name | object.category.id, object.category.name |
users.booking_owner | user |
users.student, left out when there is none | student, null when there is none |
user_mobile | mobile |
is_primary (1/0) | is_primary (true/false) |
Filter registration | object_registration |
Filter is_primary (1, 0) | is_primary (true, false) |
Flight log
| v4 | v5 |
|---|---|
| No date of its own; only inside the four times | date |
object.object_registration, object.object_organization_id, object.object_organization_name | object.registration, object.organization.id, object.organization.name |
type_of_flight | flight_type (null when the flight has none) |
departure.blockoff_datetime, departure.takeoff_datetime, arrival.landing_datetime, arrival.blockon_datetime (...+00:00) | departure.blockoff_at, departure.takeoff_at, arrival.landing_at, arrival.blockon_at (...Z) |
departure.tachometer_out, departure.hobbs_out, arrival.tachometer_in, arrival.hobbs_in | departure.tachometer, departure.hobbs, arrival.tachometer, arrival.hobbs |
users: a list with role Pilot, Instructor, Scout | pilot, instructor, scout: one object each, or null |
Crew member: organization_id, organization_name | Crew member: organization.id, organization.name (organization is null when not known) |
Hidden crew: users is one entry with id -1 and the name "Undisclosed by integrity settings" | crew_hidden is true, and pilot, instructor and scout are null |
instruction.is_solo: the stored code, "EK" for a student's solo flight, "DK" for a flight that is not one, or null | is_solo_instruction: true (EK), false (DK), or null when not filled in |
uplift.fuel_uplift, uplift.oil_uplift | uplift.fuel, uplift.oil |
summary.total_block, summary.total_block_minutes, ... | totals.block, totals.block_minutes, ... (the same for airborne, tach and hobbs) |
summary.number_of_flights | number_of_flights |
daily_check (1/0) | daily_check (true, false, or null) |
created_at_utc | created_at |
Filter has_instructor (0, 1, 2), has_remark (1, 0) | true, false |
verbose=true for the whole flight | Removed. A flight has no blocks: everything verbose gave is always there. Sending verbose gives 400. exclude=uplift,totals leaves fields out that are not needed. |
A flight that passes midnight: each time that is earlier than the time before it is on the next day. v4 only compared a time with the one directly before it, so with a time missing in between (block off 23:10, no take-off or landing, block on 01:20) v4 put the later time on the same day. v5 puts it on the next day.
Flight types a user may log
GET /users/{id}/approved-flight-types. The list of flight types, GET /flight-types, has the same fields as in v4.
| v4 | v5 |
|---|---|
The user as a parameter: ?user_id=5 | The user's id in the address: /users/5/approved-flight-types |
The list under approvedflighttypes | The list under data |
The object as id and registration on the item itself | object.id, object.registration |
available_flight_types, left out for an object without any | flight_types, [] for an object without any |
Transactions
| v4 | v5 |
|---|---|
organization_id | Removed. It is always the token's own organization. |
created_ts (server time, 2026-10-03 12:00:00) | created_at (UTC, 2026-10-03T10:00:00Z) |
include_account_rows=true adds accounting.rows | include=account_rows adds account_rows |
include_account_summary=true adds accounting.summary | include=account_summary adds account_summary |
account as a number | account as text |
debit and credit can have many decimals when sums are not exact | Always at most two decimals |
| A summary row can have 0 as both debit and credit | Such rows are left out |
Filter date | The same, an exact date |
Filter amount | The same, a number with at most two decimals |
Creating a transaction
POST /transactions.
| v4 | v5 |
|---|---|
accounting.debit, accounting.credit (number or text) | accounting.debit_account, accounting.credit_account, text with digits: "1930" |
amount as a number or as text | A number with at most two decimals |
user_id as 5 or "5" | A number |
date: anything that could be read as a date | YYYY-MM-DD |
| Checks stopped at the first error | All errors at once, each with its field |
Reply 200 with a list of one transaction under transactions | Reply 201 with the transaction under data, and its address in the Location header |
| Unknown user: 400 with code 10100 | 400 with user_id in errors (code 10100) |
| Duplicate (same user, date, amount and comment): 400 | 409 (code 10130). The check itself is unchanged. |
| Date before the lock date: 400 | 409 (code 10150). The reply names the lock date. |
The renamed account fields avoid a mix-up with debit and credit in a reply, which are amounts.
Login check
POST /login-checks, with username and password as before.
| v4 | v5 |
|---|---|
Right login: 200 with logincheck: {user_id, organization_id, username, user_fullname, user_firstname, user_lastname_particle, user_lastname} | 200 with data: {"valid": true, "user": {id, username, name, first_name, last_name_particle, last_name}} |
Wrong login: 401 with logincheck: false | 200 with data: {"valid": false, "user": null} |
| Username or password missing: treated as a wrong login (401) | 400, naming the field |
Module login_check | The same token right. It is listed as login_checks in GET /token. |
Too many failed logins for a username still give 429 with code 10012 and a Retry-After header.
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.