Skip to content

GraphQL API

Mobilizon features a GraphQL API which allows to pragmatically interact with the application: search events, create events and virtually any operation that is possible in the WebUI. Authentication is handled with Json Web Tokens. Each instance provides a GraphQL playground at /graphiql, such as https://mobilizon.fr/graphiql that you can use to explore the schema. You can also use the schema's representation with a tool like graphdoc.

The API is accessible on the /api route of a Mobilizon instance. Be careful that the API can be different from one version of Mobilizon to another. Test you code before upgrading.

Authentication

Some operations or accessing some contents require authentication and appropriate authorization (like group membership for example). You will need to make these calls using a

Follow the API auth section to know how to create an application and authenticate.

Users and profiles

The access token is linked to a user (an email). But in Mobilizon, the user is never directly exposed. A user can have one or many profiles (one identity for each kind of activities she/he is involved for example).

Profiles and groups are both "Actors" in Mobilizon.

When interacting with the API, it my be required for some operations to provide an "ActorID".

query:

query Identities {
  identities {
    ...ActorFragment
  }
}

fragment ActorFragment on Actor {
  id
  type
  preferredUsername
  name
  url
}

There is no variables to provide as it queries the identities (profiles) of the owner of the access token.

response:

The response is the list of all your profiles

{
  data: {
    identities: [
      [0]: {id: "123", name: "Profile 1", preferredUsername: "profile_1", type: "PERSON", url: "https://keskonfai.fr/@profile_1"},
      [1]: {id: "456", name: "Profile 2", preferredUsername: "profile_2", type: "PERSON", url: "https://keskonfai.fr/@profile_2"},
      [2]: {id: "789", name: "Profile 3", preferredUsername: "profile_3", type: "PERSON", url: "https://keskonfai.fr/@profile_3"},
      [3]: {id: "945", name: "Profile 4", preferredUsername: "profile_4", type: "PERSON", url: "https://keskonfai.fr/@profile_4"}
    ]
  }
}

Get my groups

It is recommanded to create events under a group instead of under a person, in order to favor communities of interest.

For that it is necessecary to be able to get identifiers of ones own groups.

Query

query LoggedUserMemberships($membershipName: String, $page: Int, $limit: Int) {
  loggedUser {
    id
    memberships(name: $membershipName, page: $page, limit: $limit) {
      total
      elements {
        role
        actor {
          ...ActorFragment
        }
        parent {
          ...ActorFragment
        }
      }
    }
  }
}

fragment ActorFragment on Actor {
  id
  type
  preferredUsername
  name
}

Variables

Only variables for pagination are possible here as the user id is taken from the authentification token. If user have a lot of groups, it can be useful to query them all at once by changing the default page size (10), ie { "limit": 20 }.

Response

Response is a list of members, consisting in an association of profiles, groups and roles.

For readability, the JSON response can be formated to a table using jq -r '.data.loggedUser.memberships.elements[]|[.actor.id, .actor.name, .parent.id, .parent.name, .role]|@tsv' | column -t -s $'\t' -N 'profile_id,profile,group_id,group,role'.

This outputs a table as following:

profile_id  profile        group_id  group       role
5           profile_1      65835     group_A     ADMINISTRATOR
5           profile_1      147460    group_B     MEMBER
68          profile_2      1140592   group_C     ADMINISTRATOR
68          profile_2      1143591   group_D     ADMINISTRATOR
68          profile_2      356464    group_E     MEMBER
122         profile_3      899048    group_F     MEMBER
109687      profile_4      1125368   group_G     ADMINISTRATOR
292071      profile_5      292123    group_H     MODERATOR

A row must be read as : current user, under profile_1 is administrator of group_A. Also under profile_3, is member of group_F, etc.

When creating an event for a group:

  • variable organizerActorId must be set to profile_id
  • variable attributedToId must be set to group_id.

Create an event

This operation requires an authentication and an actor id.

Mutation

mutation createEvent(
  $organizerActorId: ID!
  $title: String!
  $attributedToId: ID
  $description: String!
  $beginsOn: DateTime!
  $endsOn: DateTime
  $status: EventStatus
  $visibility: EventVisibility
  $joinOptions: EventJoinOptions
  $draft: Boolean
  $tags: [String]
  $picture: MediaInput
  $onlineAddress: String
  $phoneAddress: String
  $category: String
  $physicalAddress: AddressInput
  $options: EventOptionsInput
  $contacts: [Contact]
) {
  createEvent(
    organizerActorId: $organizerActorId
    attributedToId: $attributedToId
    title: $title
    description: $description
    beginsOn: $beginsOn
    endsOn: $endsOn
    status: $status
    visibility: $visibility
    joinOptions: $joinOptions
    draft: $draft
    tags: $tags
    picture: $picture
    onlineAddress: $onlineAddress
    phoneAddress: $phoneAddress
    category: $category
    physicalAddress: $physicalAddress
    options: $options
    contacts: $contacts
  ) {
    id
    uuid
  }
}

Variables

Only title and start date are mandatory but all fields are useful. Here is an example :

{
  "organizerActorId": 68,
  "attributedToId": 356464,
  "title": "A l'école des explorateurs - Visite guidée Uzès, Ville d'art et d'histoire",
  "beginsOn": "2020-10-29T00:00:00+01:00",
  "endsOn": "2022-03-31T23:59:59+02:00",
  "visibility": "PUBLIC",
  "description": "<p>Accessible en fauteuil roulant en autonomie.</p>",
  "draft": false,
  "joinOptions": "FREE",
  "local": true,
  "onlineAddress": "https://www.tourismegard.com/",
  "options": {
    "commentModeration": "CLOSED",
    "showStartTime": false,
    "showEndTime": false
  },
  "phoneAddress": null,
  "status": "CONFIRMED",
  "tags": ["culturel"],
  "physicalAddress": {
    "description": ["Chapelle des Capucins"],
    "geom": "4.419471;44.013957",
    "locality": "Uzès",
    "postalCode": "30700",
    "street": "16 place Albert 1er",
    "country": "France"
  },
  "picture": {
    "media_id": "180"
  }
}

Create an event for a group

When variable attributedToId is not null, the event will be attributed to the group with the provided id. The provided actor must have the right to publish events for this group (having role member, moderator or administrator) for the mutation to be accepted.

Response

We could ask for any parts of the event, but here we asked to return id and uuid only as they are not known in advance and can be useful.

id is the internal identifier of the event in the platform. It is required to update or delete the event.

uuid is the public identifier of the event that is present in the URL when accessing the event page directly (eg: https://mobilizon.fr/events/e2464054-60f3-47d8-b6aa-68c810107068)

It may be important to store these information for further usage in your business logic and probably to link them to the identifier of the same event in your own information system.

Update an event

Mutation

Updating an event is very similar to create it. The payload is the same, with the parts that are to be updated and the parts that are not changing.

mutation updateEvent($id: ID!, ...) {
  updateEvent(eventId: $id, ...) {
    id
    uuid
  }
}

Variables

In addition to what is required for a creation, an update require the id of the event.

Response

Same as for creation.

Delete an event

Mutation

mutation DeleteEvent($eventId: ID!) {
  deleteEvent(eventId: $eventId) {
    id
  }
}

Variables

Only the id of the event is needed to perform a deletion.

Response

We don't need anything, but we can check the id to make sure it matches the one given as variable.

Search events

This operation does not require an authentication. However, without authentication, only public content is accessible.

Query

query SearchEvents(
  $location: String
  $radius: Float
  $tags: String
  $term: String
  $type: EventType
  $beginsOn: DateTime
  $endsOn: DateTime
  $eventPage: Int
  $limit: Int
) {
  searchEvents(
    location: $location
    radius: $radius
    tags: $tags
    term: $term
    type: $type
    beginsOn: $beginsOn
    endsOn: $endsOn
    limit: $limit
  ) {
    total
    elements {
      id
      title
      uuid
      beginsOn
      picture {
        id
        url
        name
        __typename
      }
      status
      tags {
        ...TagFragment
        __typename
      }
      physicalAddress {
        ...AdressFragment
        __typename
      }
      organizerActor {
        ...ActorFragment
        __typename
      }
      attributedTo {
        ...ActorFragment
        __typename
      }
      options {
        ...EventOptions
        __typename
      }
      __typename
    }
    __typename
  }
}

fragment EventOptions on EventOptions {
  maximumAttendeeCapacity
  remainingAttendeeCapacity
  showRemainingAttendeeCapacity
  anonymousParticipation
  showStartTime
  showEndTime
  timezone
  offers {
    price
    priceCurrency
    url
    __typename
  }
  participationConditions {
    title
    content
    url
    __typename
  }
  attendees
  program
  commentModeration
  showParticipationPrice
  hideOrganizerWhenGroupEvent
  isOnline
  __typename
}

fragment TagFragment on Tag {
  id
  slug
  title
  __typename
}

fragment AdressFragment on Address {
  id
  description
  geom
  street
  locality
  postalCode
  region
  country
  type
  url
  originId
  timezone
  __typename
}

fragment ActorFragment on Actor {
  id
  avatar {
    id
    url
    __typename
  }
  type
  preferredUsername
  name
  domain
  summary
  url
  __typename
}

The query is big because there are many parts in an event object : picture, location, organizer,group, tags, participants, in addition to the basic fields, title, dates, etc.

It is not mandatory to request all parts. It really depends on what information are needed in the response processing phase.

Variables

{
  "beginsOn": "2022-02-28T23:00:00.000Z",
  "endsOn": "2022-03-31T21:59:59.999Z",
  "eventPage": 2,
  "limit": 12,
  "radius": 100,
  "term": "culture",
  "location": "u05cyfxcr"
}

It is possible to search by :

  • time window, ie between two dates, using beginsOn and endsOn, both are optional
  • distance from a location (location in geohash, radius in km)
  • term, in title or description (title has more weight than description for ranking)
  • or a combination of any of them

By tags

{
  "eventPage": 3,
  "limit": 12,
  "radius": null,
  "tags": "gratuit"
}

Notice : if a list of tags (comma separated) is provided in the request, they will be used as "or" query not "and".

Pagination

Pagination is achieved using eventPage (page number) and limit (page size) variables.

Their is a hard limit on the number of elements sent in the response and a default limit too, usually 10.

Processing response

response is composed of:

  • the total of elements corresponding to the query
  • the elements of the requested page
▽ data: {searchEvents: {…}}
  ▽ searchEvents: {__typename: "Events", elements: […], total: 221}
      __typename: "Events"
    ▽ elements: [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
      ▷ [0]: {__typename: "Event", beginsOn: "2021-12-31T23:00:00Z", id: "690367", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
      ▷ [1]: {__typename: "Event", beginsOn: "2022-01-01T05:00:00Z", id: "737870", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
      ▷ [2]: {__typename: "Event", beginsOn: "2022-01-31T23:00:00Z", id: "717177", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
      ▷ [3]: {__typename: "Event", beginsOn: "2022-01-31T23:00:00Z", id: "739045", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
      ▷ [4]: {__typename: "Event", beginsOn: "2022-02-24T23:00:00Z", id: "755134", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
      ▷ [5]: {__typename: "Event", beginsOn: "2022-02-28T19:00:00Z", id: "759028", options: {…}, organizerActor: null, physicalAddress: {…}, picture: null, stat…: …, …}
      ▷ [6]: {__typename: "Event", beginsOn: "2022-03-01T07:00:00Z", id: "714563", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
      ▷ [7]: {__typename: "Event", beginsOn: "2022-03-01T11:15:00Z", id: "759637", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
      ▷ [8]: {__typename: "Event", beginsOn: "2022-03-01T11:30:00Z", id: "759552", options: {…}, organizerActor: null, physicalAddress: {…}, picture: null, stat…: …, …}
      ▷ [9]: {__typename: "Event", beginsOn: "2022-03-01T13:00:00Z", id: "771678", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
      ▷ [10]: {__typename: "Event", beginsOn: "2022-03-01T18:40:00Z", id: "762675", options: {…}, organizerActor: null, physicalAddress: {…}, picture: null, sta…: …, …}
      ▷ [11]: {__typename: "Event", beginsOn: "2022-03-02T07:00:00Z", id: "714023", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, stat…: …, …}
      total: 221

They can be used to display a list, thumbnails in a grid, a map, etc.

Media upload

Mutations that include a file upload use a special encoding.

The request body is encoded with the Content-Type multipart/form-data, with (at least) three named parts:

  • query: The raw graphql query (not a JSON encoded object).
  • variables: A JSON-encoded variables object. The values for fields of type Upload do not contain the file, but an arbitrary ID, which references the form-data field where the uploaded file is contained.
  • <id>: The file content prefixed with Content-Type: application/octet-stream header.

Standardization Efforts

There is a "spec" for this general problem, though this is not what mobilizon implements.

Quirks

The multipart delimiter must follow the format ---------------------------164507724316293925132493775707 This means that the delimiters generated by default by golang's mime/multipart package are not accepted.

Clients

Even though a request handler for this is quickly written in most languages, clients known to follow this request structure out of the box are:

Example request body

-----------------------------164507724316293925132493775707
Content-Disposition: form-data; name="query"

mutation uploadMedia($file: Upload!, $name: String!) {
    uploadMedia(file: $file, name: $name) { id }
}
-----------------------------164507724316293925132493775707
Content-Disposition: form-data; name="variables"

{"name":"foo.jpg","file":"image1"}
-----------------------------164507724316293925132493775707
Content-Disposition: form-data; name="image1"; filename="foo.jpg"
Content-Type: image/jpeg

����JFIFHH��&vExifII*
�
�▒�(

[...]
-----------------------------164507724316293925132493775707

Debugging and development

Inspect web application in a web browser

When struggling with the API, keep in mind that the Web application is using it. So you can still spy on it. In a web browser, use developer tools (F12 in Firefox) to look at the requests that are sent to the API by the Webapp when performing an action, and the response that is sent back by the server. For the request, it will be GraphQL query format, with variables as JSON. For the response, it will be JSON format.

Low-level tools

Before moving to a high level programming language and facing bugs, it is a good idea to test API calls with a lower level tools, such as :

  • Curl a command line HTTP client included in any Linux distribution and other systems
  • HTTPie an other command line HTTP client
  • podman, not open source but specialized in API testing
  • graphiql playground already mentioned
  • you name it

Programming language integrations

Support

You can seek for support on the forum or the chat.

Have fun !


Last update: August 30, 2023