Mobile API deprecated

Mobile API

The Mobile API is a resource-oriented HTTP API for native mail apps, served under /mobile/v1. Its contract with app developers is the OpenAPI document mobile-api/openapi/mobile-api-v1.yaml. This page covers what an operator sets up: the switch, the route, the sign-in mode, and how to check the installation.

Enabling the API

The API ships in the package open-xchange-mobile-api, which depends on open-xchange-core and open-xchange-oauth-provider and which the Helm chart keeps switched off through the feature mobile-api. Two switches, both off by default:

features:
  status:
    mobile-api: enabled

properties:
  com.openexchange.mobile.api.enabled: "true"

The feature starts the bundle, the property lets it answer. To keep the API to the pods that serve clients, put the feature under that role instead, e.g. roles.http-api.values.features.status.mobile-api: enabled.

While the property is false at server level, every request under /mobile/v1 gets 404. The property is config-cascade aware: set to false for a context or user, it keeps them out with 403 and code feature_unavailable.

The API validates every bearer token through the OAuth provider, which therefore has to be enabled:

properties:
  com.openexchange.oauth.provider.enabled: "true"

Without it the bundle waits, /mobile/v1 answers nothing, and a minute after start the log names the missing service:

Mobile API not registered: bundle com.openexchange.mobile.api still waits after 60 s for com.openexchange.oauth.provider.resourceserver.OAuthResourceService; the OAuth provider is switched off (com.openexchange.oauth.provider.enabled) or failed to start

All properties are reloadable and read per request; a configuration reload applies without a restart.

Reaching it

The API answers on the middleware's HTTP port, next to the AJAX API. Route /mobile/v1 from the public host name to the same service the AJAX API uses. The core-mw chart has no ingress of its own; the route belongs to the installation's ingress. Where that lists its routed prefixes explicitly, as with /mcp and /scim, add /mobile there, or requests end in the ingress's own 404.

GET /mobile/v1/auth/config returns the public base URL as apiBase, built from the Host header and the scheme the HTTP engine reports (which honors X-Forwarded-Proto), never from X-Forwarded-Host. The route must pass the public Host through. The answer is sent with Cache-Control: no-cache and an ETag: caches revalidate it on every use, so a changed sign-in mode applies at once and no cache serves one client's answer to another.

An authentication layer in front of the middleware, such as edge-auth, has to reject a token on /mobile/v1/* the way the middleware does: 401 with WWW-Authenticate: Bearer error="invalid_token" and an application/problem+json body with code invalid_token. Apps sign in again only on that answer; they treat any other 401 as a network problem.

Apps follow two rules from ADR-044: they send Accept: application/problem+json together with application/json on every request, and they pick the Bearer challenge when WWW-Authenticate carries more than one. The middleware sends the problem body whatever Accept says, with a single challenge. A layer in front may send the body only when Accept asks for it and may add challenges of its own; to an app that follows both rules, the two behave the same.

Sign-in modes

Each deployment uses exactly one mode, set with com.openexchange.mobile.auth.mode. A token of the other kind is answered with 401.

Mode Token What to configure
token (default) Personal access tokens (oxa_…) the app gets with user name and password Nothing beyond the OAuth provider.
idp Access tokens of the operator's identity provider The provider's validation (com.openexchange.oauth.provider.mode, .jwt.* or .introspection.*), plus com.openexchange.mobile.auth.idp.issuer and .clientId, which auth/config publishes to the app. auth/config answers 500 while either is missing.

In mode idp, the provider has to map the identity provider's scopes to the scopes the operations require: read_mail, write_mail and read_contacts. A token without the scope an operation requires gets 403 with code insufficient_scope. com.openexchange.mobile.auth.idp.audience only tells the app what to request; the middleware checks a token's audience only when com.openexchange.oauth.provider.jwt.audience is set, which applies to every endpoint validating tokens.

Under external authentication (com.openexchange.external.authentication.enabled=true), an authentication service in front of the middleware, such as edge-auth, signs users in and mints the tokens. The middleware has no password sign-in then, so the API always runs in mode idp, whatever com.openexchange.mobile.auth.mode says, and logs a warning if that names token. The app still signs in at the identity provider that .idp.issuer and .idp.clientId name. The middleware sees only the token the authentication service passes on. That token carries the service's own audience and names no app in azp (accesstoken for a personal access token), so .idp.checkAudience does not apply: any token the service passes on is accepted, within its scopes. Restrict its tokens to the middleware with com.openexchange.oauth.provider.jwt.audience.

In mode token, auth/config lists the second factors whose providers are installed and enabled at server level (com.openexchange.multifactor.totp.enabled, com.openexchange.multifactor.backupString.enabled, com.openexchange.multifactor.sms.enabled). A context or user level setting cannot show there, since the caller is not known yet.

Property Default Meaning
com.openexchange.mobile.api.enabled false Whether the API answers; config-cascade aware.
com.openexchange.mobile.auth.mode token token or idp.
com.openexchange.mobile.auth.idp.issuer empty Mode idp: the OpenID Connect issuer.
com.openexchange.mobile.auth.idp.clientId empty Mode idp: the public client identifier of the app.
com.openexchange.mobile.auth.idp.scopes empty Mode idp: comma-separated scopes the app requests.
com.openexchange.mobile.auth.idp.audience empty Mode idp: the resource indicator, if the identity provider needs one.
com.openexchange.mobile.auth.idp.redirectUris.ios empty Mode idp: the redirect URI of the iOS app.
com.openexchange.mobile.auth.idp.redirectUris.android empty Mode idp: the redirect URI of the Android app.

Signing in with a password (mode token)

POST /mobile/v1/auth/token exchanges user name and password, and a second factor if the user has one, for a personal access token. It runs on the access token sign-in of the middleware, which is off by default:

properties:
  com.openexchange.accesstoken.signin.enabled: "true"
  • The sign-in counts as login of the client open-xchange-mobile-api. If com.openexchange.accesstoken.signin.allowedClients is set, it has to list that client.
  • Rate limits, the default scopes and the default lifetime come from com.openexchange.accesstoken.signin.*. A lifetime the app asks for is capped at the user's com.openexchange.accesstoken.maxLifetimeDays.
  • The token is named after the device the app sends. A user for whom com.openexchange.mobile.api.enabled is false gets 403 with code feature_unavailable and no token.
  • A user who already has com.openexchange.accesstoken.maxTokensPerUser tokens gets 403 with code permission_denied; tokens of devices that were reset without signing out count until they expire or the user revokes them in the web client.

GET and DELETE /mobile/v1/auth/token work with any personal access token, whether the sign-in is enabled or not. Signing out revokes the token and drops the mail credential kept for it.

Rotating the token

POST /mobile/v1/auth/token/rotate issues a new secret for the presented token and gives the token its lifetime again: the new expiry is now plus the lifetime the token was minted with, capped by com.openexchange.accesstoken.maxLifetimeDays as configured now. An app that rotates once rotateAfter has passed (a quarter of the lifetime) never has to sign in again; a token on an unused device still expires. An expired token cannot be rotated.

  • Identifier, scopes, push subscriptions and the kept mail credential stay; scheduled mails are sent as planned. The copies of the credential that push and scheduled sending keep and that would have ended with the token move on with it.
  • The presented secret stays valid for 300 seconds on every node, or until the next rotation; then only the new one is. A retry with the same secret and Idempotency-Key within that time gets the same new secret. The result kept for the retry is sealed with the presented secret, so the idempotency store never holds the new one readable. With com.openexchange.ajax.idempotency.enabled switched off there is no such retry: an app that loses the answer has to sign in again.
  • Of two rotations with the same secret, only one succeeds; the other gets 409 with code conflict. A rotation with a secret that another rotation replaced more than a minute earlier revokes the token and is logged at WARN: the device and someone holding a copy of the secret cannot be told apart.
  • The token's mail credential stays sealed with its first secret, which serves as its key from the first rotation on; the vault row is not rewritten.
  • The tables need the update task AccessTokenAddRotationColumnsTask. During a rolling upgrade, nodes of the previous version know neither the operation nor the grace period: requests with the replaced secret that reach them get 401.

In mode idp these operations answer 404: the app signs in and out at the identity provider. Personal access tokens minted before a switch from token to idp stay valid for other endpoints that accept them, e.g. the HTTP API with a bearer token, until they expire, and the app can no longer revoke them here. Before such a switch, let users sign out, or have them revoke the tokens in the web client.

External mail accounts

GET /mobile/v1/mail/accounts also lists a user's external mail accounts. Their passwords are encrypted with the secret that com.openexchange.secret.secretSource names. With the default "<password>" that is the user's login password, which a request with an access token does not carry itself:

  • Mode token, signed in with the password of the primary mailbox: the token keeps that password, sealed so that only the token's secret opens it. On requests with the token, <password> resolves to it, so external accounts need nothing further. The password is decrypted for each use; it is neither put into the session nor cached.
  • Mode idp, master authentication (com.openexchange.mail.passwordSource=global) and OAuth login to the mail server: there is no password. Choose a secret source that does not depend on it, as for OAuth logins with com.openexchange.authentication.oauth.keepPasswordInSession=false. Changing it affects the passwords already stored; see Secret source.

An account whose password cannot be decrypted shows state secret_unavailable; requests for its folders and messages get 403 with code mail_access_unavailable. In mode token this also applies to a token issued before the user changed the password: it keeps the former one, and signing in again issues a token with the current one. The Mobile API itself stores no account passwords, but reading one that only a former secret opens (<list>, or a legacy encryption) re-encrypts it with the current secret; with such a token, that is the former password. The web client then offers the recovery of the secret.

Scheduled sending

Me.capabilities lists scheduled_send, and sendAt is accepted, when scheduled mail is available to the user as in the web client (com.openexchange.mail.scheduled.enabled; without token exchange or master authentication also com.openexchange.mail.scheduled.requireMasterAuth=false), drafts are kept in the drafts folder, and the middleware can keep what sends the mail later:

  • Mode token: a copy of the token's mail credential. It is released with the token, so revoking the token (or its expiry) before the date to send makes the scheduled mail fail; the user is notified by mail. A rotation keeps the copy.
  • Mode idp: tokens from the token exchange configured for scheduled mail, or master authentication (com.openexchange.mail.passwordSource=global). Otherwise the capability is missing and sendAt is refused with 403.

Scheduled mails are the same as those of the web client: they wait in the scheduled folder and are listed, sent and canceled the same way.

Retries

Writes that the spec marks with Idempotency-Key keep their result, so that an app retrying after a lost response gets the first answer, marked Idempotent-Replayed: true, instead of performing the write twice. The key is scoped to the access token in mode token and to user and OAuth client in mode idp. This uses the same store and settings as the HTTP API: com.openexchange.ajax.idempotency.enabled, .retention (default 24h), .sendRetention for sending (default 7d), .maxResultSize and .claimTimeout. Switched off, writes are performed without this protection. With the store unreachable, a write that requires a key, sending above all, answers 503 with Retry-After and performs nothing; one with an optional key is performed without protection. A write that requires a key still answers 428 without one.

Push notifications

Apps register a device for APNs or FCM as a push subscription. The registration names an appId; the push credentials are looked up for the client open-xchange-mobile-<appId>. Nothing checks at registration that they exist: without them the registration succeeds, but no push is sent, and the log shows No such transport or NOT enabled at INFO level for every notification.

For each app, configure the push clients in /opt/open-xchange/etc/pushClientConfig (see Push client configuration), with the suffix the transport appends:

open-xchange-mobile-com.example.mail-ios:         # APNs, App Store and TestFlight builds
  _type: apn
  authtype: jwt
  topic: com.example.mail
  production: true
  privatekeyId: example-mail-apns
  keyid: MyKeyId
  teamid: MyTeamId

open-xchange-mobile-com.example.mail-sandbox-ios: # APNs, development builds (environment "sandbox")
  _type: apn
  authtype: jwt
  topic: com.example.mail
  production: false
  privatekeyId: example-mail-apns
  keyid: MyKeyId
  teamid: MyTeamId

open-xchange-mobile-com.example.mail-android:     # FCM
  _type: fcm
  name: example-mail
  keyPath: /opt/open-xchange/etc/example-mail-fcm.json

and allow the transports for these clients:

com.openexchange.pns.transport.apns_http2.ios.enabled.open-xchange-mobile-com.example.mail=true
com.openexchange.pns.transport.apns_http2.ios.enabled.open-xchange-mobile-com.example.mail-sandbox=true
com.openexchange.pns.transport.fcm.enabled.open-xchange-mobile-com.example.mail=true

What a new-message push shows is the operator's choice: com.openexchange.mobile.push.notificationContent is the default (none), com.openexchange.mobile.push.maxNotificationContent the most a device may ask for (senderAndSubject). A push names the changed folders by their Mobile API identifiers, but carries no message identifiers: these need the mail server, which a push has no session for. The folders a subscription is limited to are resolved when it is made or changed. Renames and moves within an account through the Mobile API or App Suite are followed; after a rename by another mail program the folder gets no pushes until the app registers again, which it does on every start. The same holds for a folder renamed at the moment the app adds it to a subscription. com.openexchange.mobile.push.maxSubscriptionsPerUser limits the subscriptions per user (20).

Which mail events reach a device depends on where they come from:

  • New messages are announced by the mail server, through Dovecot push or JMAP, or by a permanent IMAP IDLE listener of mail push, which needs com.openexchange.push.allowPermanentPush=true and com.openexchange.push.imapidle.supportsPermanentListeners=true. Such a listener needs a credential to reach the mailbox without the app: under master authentication it needs none; otherwise it keeps one in the mail credential vault, which requires com.openexchange.push.credstorage.enabled=true. In mode token that is a copy of the credential of the app's access token, released when any access token of the user is revoked; the app's next registration keeps a new one. In mode idp it is obtained through a token exchange at the identity provider, configured with com.openexchange.mail.credential.oauth.tokenExchange.push=true and …tokenExchange.push.backendPath, and only for tokens issued to com.openexchange.mobile.auth.idp.clientId, the app's client; without one, and without master authentication, devices get no new-message pushes from IMAP IDLE.
  • Changed and deleted messages and changed folders are announced by the middleware when they are changed through App Suite or the Mobile API, for the primary account only. The app finds other changes when it syncs.

A subscription made with an access token ends when the token is revoked, e.g. by a password change, or expires. One made in mode idp ends only when the app deletes it or after 30 days without renewal: revoking the session at the identity provider does not end it, so a lost device keeps getting pushes up to 30 days. A device token registered by another user moves to that user, together with the device's pushes.

Live events

GET /mobile/v1/events is the live change stream in the format of the Mobile API: state events carry an EventPayload with the API's folder identifiers and the topics mail.message.created, .changed, .destroyed and mail.folder.changed; there is no hello and no close event. Instead a stream starts and ends with a bare retry line, which tells the client when to reconnect without being an event: 1 to 10 seconds at random, 60 seconds after the stream was replaced or is no longer permitted. It needs scope read_mail and the SSE transport (package open-xchange-pns-impl), switched on with com.openexchange.pns.transport.sse.enabled. Where that is false for a user, the stream answers 404. HEAD answers 405.

Lifetime and ping interval are those of the transport. maxConnectionsPerUser applies per device: to the streams of one access token in mode token, of one Push-Subscription-Id in mode idp, and to all streams without that header together. A device never replaces the streams of another device or of the web client. An open stream ends within tokenCheckInterval once its token is revoked or loses read_mail, or the API is switched off for the user.

A stream skips the changes of its own device: in mode token those made with the same access token, in mode idp those whose request named the same Push-Subscription-Id as the stream.

Batch

POST /mobile/v1/batch runs up to 20 operations in one round trip, one after another, each with the scope, Idempotency-Key and error handling of a single call. Operations with binary content cannot be part of a batch. Limits:

  • A batch request takes at most 1 MiB; a larger one is answered with 413.
  • The references of one entry expand to at most 1 MiB; an entry beyond that fails with 413 and does not run.
  • The bodies of all answers take at most 8 MiB together. An entry whose body no longer fits keeps its status, since it ran, and reports Batch-Body-Omitted: payload_too_large instead of the body; once the 8 MiB are used up, the remaining entries fail with 413 without running.

An Idempotency-Key in an entry is bound to the entry as sent, references unresolved: a retry of the batch replays the entry even if the results it refers to changed in between.

An entry with forEach runs once per element of an array from an earlier result, e.g. once per folder. All such runs of one batch together are bounded by com.openexchange.mobile.api.batch.maxFanOut (default 50, config-cascade aware); an entry beyond fails with 413 without running. Each run counts against com.openexchange.mobile.api.rateLimit.requests like an entry.

Limits per user

Each node bounds what one user may do, across all of the user's devices. Beyond a limit a request is answered with 429, code rate_limited and Retry-After, before it does anything. All properties are config-cascade aware; 0 switches a limit off.

Property Default Bounds
com.openexchange.mobile.api.rateLimit.requests 50 Requests per window; every entry of a batch, every run of a fan-out and every account of a search across accounts counts.
com.openexchange.mobile.api.rateLimit.windowSeconds 10 The window of the above, at most 600.
com.openexchange.mobile.api.rateLimit.maxConcurrent 20 Requests running at the same time.
com.openexchange.mobile.api.idempotency.maxKeys 5000 Writes with a new Idempotency-Key per day, whose results Redis keeps.
com.openexchange.mobile.api.idempotency.maxBytes 52428800 Bytes of such results per day; sending is not refused over them.
com.openexchange.mobile.api.uploads.maxConcurrent 4 Uploads running at the same time, each spooled to local disk until stored.

The limits apply per node, so behind a load balancer a user gets them on every node that serves him. In the load test below, the 40 devices of one user made about 3.5 requests per second at peak, batch entries counted, which is about 70 to 75% of the default budget of 50 per 10 seconds. Measure before lowering it.

Sizing

The figures in this section are guide values from a load test on a single development machine: one node with 2 GiB heap, with Dovecot, MariaDB and Redis on the same machine (see tools/mobile-api-loadtest). Measure your own installation before you rely on them.

Event streams

An open stream holds no thread. It keeps a queue of at most 100 events, a cache of at most 256 folder identifiers and a mail push listener for its session; changes are translated to the API's identifiers by at most 32 virtual threads per node. Measured: about 14 KB of heap per open stream, and 20 to 35 KB per device that made a request (mostly its session).

On the Jetty engine, a connection that served more than one request also keeps a header cache, sized by com.openexchange.http.jetty.headerCacheSize (256 entries). The connection of a stream usually is one, since apps reconnect over kept-alive connections, so in steady operation a stream costs about 25 KB: 1000 streams take about 25 MB. With Jetty's own default of 1024 entries it was about 110 KB.

Property Default Meaning
com.openexchange.pns.transport.sse.maxConnectionsPerNode 5000 Streams per node; beyond that a stream is refused with 503.
com.openexchange.pns.transport.sse.maxConnectionsPerUser 5 Streams per device; a new one closes the oldest.
com.openexchange.pns.transport.sse.pingInterval 30000 Milliseconds between pings, at most 50 seconds. Idle timeouts of load balancers have to be longer.
com.openexchange.pns.transport.sse.maxLifetime 1800000 Milliseconds until a stream is closed and the app reconnects.

Each stream is also a connection through the ingress and a file descriptor on the node; those, not the heap, usually set the limit. Plan for the streams of the devices in the foreground at the same time, e.g. 1000 of 2000 active devices.

Redis

Nothing grows with the number of messages: message identifiers are encoded, not mapped. What the API keeps:

Key Per Size Kept
ox-map:idempotencyKeys:<context>/<user>/… write with Idempotency-Key The answer, up to com.openexchange.ajax.idempotency.maxResultSize (64 KB), for message-bulk up to 1 MiB; errors up to 2 KB retention (24 h), for sending sendRetention (7 days); errors 1 h
ox-map:sentMessageIds:… mail sent with a clientMessageId small com.openexchange.mail.sentMessageIds.retention (30 days)
ox-map:mobileSentDrafts:<context>:<user>:<draft> sent draft small as sentMessageIds
ox-map:mobileUploads:<context>:<user>:<upload> upload about 300 bytes until 7 days after the upload expires
ox-map:mobileUploadUsage:<context>:<user> user with uploads grows with the uploads 24 h plus 7 days
ox-map:mobileFolderSnapshots:<context>:<user>:<hash> folder list state handed out about 80 bytes per folder before Redis compression; at most 384 KiB 7 days after the last use; per user over all devices at most 50 and 1.5 MiB listed, least recently used dropped, those a client already got a successor for first; one that two devices write at the same moment may stay unlisted until it expires
ox-map:mobileFolderSnapshotIndex:<context>:<user> user who listed folders up to 50 short entries 7 days
ox-mail-folder-renames:<context>:<user>:<account> user who renamed folders through the middleware at most 1000 entries 30 days
ox-pns-sse:<context>:<user> user with open streams one entry per stream twice the ping interval

Folder snapshots back listFolderChanges. A new one is written whenever a folder list or delta differs from the last one, e.g. with every new message when counts are asked for. A user with 50 folders keeps about 4 KB per snapshot, so at most about 200 KB; the 1.5 MiB bound per user only matters for users with thousands of folders. A list of more than about 5000 folders gets no state, nor does one that leaves out an account that could not be read in time.

Each device that made a request also has a session in Redis (ox-session:… and two index keys), about 1 KB.

The idempotency results dominate. In the load test a device changed the flags of 100 messages about every 36 minutes, each leaving about 0.5 KB for a day: about 20 MB per 1000 devices, plus about 1 MB of sessions and 0.3 MB of stream presence. An app that marks messages one at a time keeps many small results instead; one that sends many mails keeps them for 7 days.

Filestore

Uploads for mails to be sent lie in the context's filestore and do not count against its quota. They are deleted 24 hours after the upload, by a job that runs hourly on one node. A user keeps at most com.openexchange.mobile.api.uploads.maxCount (100) uploads with at most com.openexchange.mobile.api.uploads.maxSize (1 GiB) together, so the upper bound per context is the number of users writing mails with attachments within a day times that size. Both properties are config-cascade aware.

A resumable upload (Upload-Complete: ?0, then PATCH per part) counts with its announced Upload-Length from its first request and expires 24 hours after it. Each part is spooled to local disk before it is appended to the filestore object, so a broken part leaves nothing behind. Local, S3 and sproxyd filestores support appending; on S3 each part rewrites the object or copies it server side, so apps should send parts of several MB rather than small ones.

Property Default Recommendation
com.openexchange.mobile.api.uploads.maxSize 1 GiB The largest mail users may send with attachments, e.g. 100 MB.
com.openexchange.mobile.api.uploads.maxCount 100 Keep.
com.openexchange.pns.transport.sse.maxConnectionsPerNode 5000 Keep as protection; size the pods for the expected streams instead.
com.openexchange.ajax.idempotency.maxResultSize 64 KB Keep; message-bulk stores up to 1 MiB regardless.
com.openexchange.accesstoken.maxTokensPerUser 50 At least the devices per user plus a margin for devices reset without signing out.

Measured load

Guide values from tools/mobile-api-loadtest against one node: 2 GiB heap (ZGC with compact object headers; the chart uses G1), Dovecot, MariaDB and Redis on the same machine, 49 users with 40 devices each (1960 access tokens), 1000 event streams held open and reconnected after maxLifetime. The mix: 55% message-changes, 12% lists of 50 messages, 20% bodies, 5% message-bulk with 100 identifiers followed by message-changes with those 100 changes, 5% cold-start batches, 3% new mails announced as the mail server does. One hour at about 30 requests per second, then ten minutes at 155 to 169.

Measure Target Measured
p95 message-changes without changes 100 ms 7 ms
p95 message-changes with 100 changes and include 300 ms 10 ms
p95 list of 50 messages 300 ms 11 ms
p95 body below 100 KB 400 ms 12 ms
p95 message-bulk with 100 identifiers 500 ms 24 ms
p95 cold-start batch (me, folders, inbox, signatures) 1 s 31 ms
Heap per open stream 50 KB 14 KB; about 25 KB with its Jetty connection
Heap per 1000 devices, streams included 200 MB about 80 MB, no growth within an hour
Redis per 1000 devices 50 MB about 21 MB, projected to a day
CPU at 100 requests per second 70% of a core 33% (54% at 169 requests per second)

A new mail reaches the open streams of its user after about 1.5 seconds: the push notification service collects changes for a second (com.openexchange.pns.delayDuration) before it sends them.

Checking the installation

The log says it is up:

Registered Mobile API at /mobile/v1; it answers when com.openexchange.mobile.api.enabled is true
Started bundle: com.openexchange.mobile.api

From outside, auth/config answers without a credential:

curl -s https://mail.example.com/mobile/v1/auth/config

A 404 in application/problem+json means the switch is off; a 404 of another kind means the route points elsewhere or the bundle has not started.

Metrics

The API registers these meters with the Micrometer registry; they appear under /metrics like every other core metric:

Meter Type Tags
appsuite.mobile.requests timer operation (method and path template, e.g. GET /auth/config, or - if no route matched), status (the HTTP status), code (the problem code, - on success, aborted if the request failed after its response was committed)
appsuite.mobile.authentications counter mode (token or idp), result (accepted, missing, wrong_kind, invalid, expired, unavailable or rejected)
appsuite.mobile.push.messages counter transport (apns or fcm), topic (e.g. mail.message.created), result (generated, shortened if texts were cut to fit, filtered by the subscription's folders, too_big if not even the smallest payload fit)
appsuite.pns.deliveries counter transport (apns_http2 or fcm), result (accepted, invalid_token if the device is gone and its subscription dropped, invalid_request for a malformed request such as a device token of the wrong format, device_mismatch for a device token of another app or sender, whose subscription is kept, rejected for another refusal, e.g. wrong credentials, failed if the gateway was not reached)

Nothing a client sends becomes a tag value: a path that matches no route is counted as -. Each entry of a batch is counted as its own operation, in addition to POST /batch. A rising wrong_kind means apps sign in with the other mode than the deployment is set to; a rising unavailable means the token validation itself fails, which apps survive, since they keep their tokens on a 503. rejected counts valid tokens whose user may not sign in, e.g. because the user or context was disabled. appsuite.mobile.push.messages counts the messages built for Apple or Google, not their delivery; appsuite.pns.deliveries counts what the gateway answered. appsuite.mobile.requests also has histogram buckets from 50 ms to 30 s, so percentiles can be computed across pods. Sign-ins for a token are counted by appsuite_accesstoken_signin_total (see Personal access tokens), open event streams by appsuite_pns_sse_streams.

The core-mw Grafana dashboard charts all of them in its Mobile API row: requests, problems, average and 95th percentile duration, the share of 503, authentications, sign-ins, push messages and deliveries, and open event streams. With extras.monitoring.alerts.enabled, the chart's PrometheusRule alerts when

  • more than 5% of a pod's requests are answered with 503 for 10 minutes (AppSuiteMobileApiUnavailable),
  • tokens cannot be validated for 10 minutes (AppSuiteMobileTokenValidationFailing),
  • the 95th percentile of successful requests, downloads, uploads, batches and event streams aside, stays above one second for 15 minutes (AppSuiteMobileApiSlow),
  • more than 20 sign-ins within 15 minutes hit the rate limit, for 15 minutes (AppSuiteAccessTokenSignInsRateLimited),
  • more than 10% of the pushes of a transport fail or are refused for 15 minutes (AppSuitePushDeliveriesFailing),
  • more than half of the pushes of a transport go to device tokens of another app for 30 minutes, which points at a push configuration that names the wrong app (AppSuitePushDevicesMismatched),

in addition to AppSuiteLiveChangeStreamsRejected for refused event streams.

Errors

Every error is application/problem+json (RFC 9457) with a stable code that apps switch on, and oxCode, the middleware's own error code, for support. instance carries the request's tracking identifier, which is also in every log line of the request; ask for it to find the request in the logs. A 503 with code temporarily_unavailable and Retry-After means a backend could not be reached, including the token validation itself; the app keeps its token.