← Back to all posts
Mobilithek mTLS Go

Pulling Germany's Mobilithek over mTLS — A Working Walkthrough

May 1, 2026 · 9 min read

Mobilithek is Germany's National Access Point — the federal hub for traffic, mobility, and AFIR EV-charging data, operated by T-Systems on behalf of the BMDV. Since the operational retirement of MDM (the Mobility Data Marketplace) in H1 2025, Mobilithek is the canonical entry point for any production-grade German mobility integration. From 2026-04-14, DATEX II is the mandatory exchange format on it.

Most public APIs use API keys. Mobilithek doesn't. It uses TLS client-certificate authentication (mTLS): your client presents an X.509 certificate during the TLS handshake, and the broker decides whether you're allowed to pull or get pushed to.

This trips up almost everyone integrating for the first time. There is no Authorization: Bearer ... header. There is no API key. If your TLS handshake fails, you get a generic connection error and a long debugging session. Here is the working walkthrough we wish we'd had.

1. Get a Mobilithek Consumer Account

Apply at mobilithek.info/registration-request. You'll fill in:

For a typical NAP integration you want data consumer. Public-data consumers don't pay; commercial-data consumers may, depending on the publisher. Provisioning takes a few days.

2. Generate Your Certificate

Once approved, the admin component lets you upload a Certificate Signing Request (CSR) and download the issued certificate. Generate the keypair locally:

# 1. Generate a 4096-bit RSA private key
openssl genrsa -out mobilithek.key 4096

# 2. Build a CSR. The CN must match the organisation name on file.
openssl req -new -key mobilithek.key -out mobilithek.csr \
  -subj "/CN=YourOrgName/O=YourOrg/C=DE"

# 3. Upload mobilithek.csr in the Mobilithek admin component.
# 4. Download the issued certificate as mobilithek.crt

You'll also need the Mobilithek root CA and any intermediate CA certs to verify the server side of the handshake.

3. File Layout

On the application server, lay the files out something like this:

/etc/napspan/mobilithek/
├── client.crt   # the certificate issued to you
├── client.key   # the private key (chmod 600)
└── ca.pem       # Mobilithek's CA bundle for server verification

Permissions matter: the private key should be chmod 600 and owned by the user the worker process runs as. Don't ship encrypted keys in production — the worker would prompt for a passphrase on every start. If you need encryption at rest, encrypt the volume, not the file.

Reference these paths from environment variables so the binary stays secret-free:

MOBILITHEK_CERT_PATH=/etc/napspan/mobilithek/client.crt
MOBILITHEK_KEY_PATH=/etc/napspan/mobilithek/client.key
MOBILITHEK_CA_PATH=/etc/napspan/mobilithek/ca.pem

4. The Broker URL Pattern

Each Mobilithek dataset has a numeric Publikations-ID. The broker URL pattern is:

https://mobilithek.info:8443/mobilithek/api/v1.0/publication/<Publikations-ID>

Notice the non-default port 8443. That's the mTLS-protected endpoint — the standard 443 is for the public website only. Pull mode means you GET the broker URL on a schedule. Push mode means you register a service endpoint of your own in the admin component, and the broker POSTs to it whenever the publication updates — you reply with a confirmation. Both modes share the same v1.0 API root.

5. A Working Go Client

Here's the minimal Go client we use to pull a Mobilithek publication. It builds a tls.Config with the client cert and CA pool, attaches it to an http.Transport, and reuses one client across all Mobilithek requests.

package mobilithek

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

func NewClient(certPath, keyPath, caPath string) (*http.Client, error) {
    cert, err := tls.LoadX509KeyPair(certPath, keyPath)
    if err != nil {
        return nil, fmt.Errorf("load client cert: %w", err)
    }
    caPEM, err := os.ReadFile(caPath)
    if err != nil {
        return nil, fmt.Errorf("read ca: %w", err)
    }
    pool := x509.NewCertPool()
    if !pool.AppendCertsFromPEM(caPEM) {
        return nil, fmt.Errorf("parse ca pem")
    }
    transport := &http.Transport{
        TLSClientConfig: &tls.Config{
            Certificates: []tls.Certificate{cert},
            RootCAs:      pool,
            MinVersion:   tls.VersionTLS12,
        },
        MaxIdleConns:        20,
        IdleConnTimeout:     90 * time.Second,
        TLSHandshakeTimeout: 10 * time.Second,
    }
    return &http.Client{
        Transport: transport,
        Timeout:   60 * time.Second,
    }, nil
}

func FetchPublication(c *http.Client, pubID string) ([]byte, error) {
    url := fmt.Sprintf(
        "https://mobilithek.info:8443/mobilithek/api/v1.0/publication/%s",
        pubID,
    )
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Accept", "application/xml")
    req.Header.Set("User-Agent", "NAPSPAN/1.0")

    resp, err := c.Do(req)
    if err != nil {
        return nil, fmt.Errorf("mobilithek request: %w", err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    if resp.StatusCode != 200 {
        return nil, fmt.Errorf("mobilithek %d: %s", resp.StatusCode, string(body))
    }
    return body, nil
}

The result is a DATEX II XML document — usually a SituationPublication or a VmsTablePublication, depending on which Publikations-ID you pulled. Hand it to your DATEX II parser and you're done with the auth side of things.

6. Common Failure Modes

"Connection reset" or "EOF" right after ClientHello

Almost always a certificate problem. The broker rejects the handshake without a TLS-level alert. Triage:

  1. Confirm the cert is the one provisioned for this environment (Mobilithek issues distinct certs for staging and production).
  2. Confirm the cert hasn't expired. Renewals are manual; nobody will email you on day 1 of expiry.
  3. Run openssl s_client -connect mobilithek.info:8443 -cert client.crt -key client.key -CAfile ca.pem and read the actual handshake output.

HTTP 403 after a successful handshake

The TLS layer accepted you, but the broker doesn't authorise this cert for the requested Publikations-ID. Either you haven't subscribed to that publication in the admin component, or your subscription was revoked. Re-check in the consumer dashboard.

HTTP 410 Gone

The publication retired. Sometimes datasets get migrated to a new Publikations-ID — especially during the MDM-to-Mobilithek tail in 2025. Check the publication catalogue.

Stale data despite a 200

Mobilithek caches some publications behind a CDN. Check the Last-Modified header and skip parsing when it hasn't moved. Don't rely on it as ground truth though — some publishers don't update it correctly.

7. Rate Limits and Politeness

Mobilithek doesn't publish hard rate limits, but in practice:

8. What You Get Back

A typical incidents publication looks like:

<d2:payloadPublication
    xsi:type="d2:SituationPublication"
    lang="de"
    xmlns:d2="http://datex2.eu/schema/2/2_0">
  <d2:publicationTime>2026-05-01T08:34:00Z</d2:publicationTime>
  <d2:publicationCreator>
    <d2:country>de</d2:country>
    <d2:nationalIdentifier>DE-Mobilithek</d2:nationalIdentifier>
  </d2:publicationCreator>
  <d2:situation id="DE_BAB7_4711">
    <d2:situationRecord
        xsi:type="d2:Accident"
        id="DE_BAB7_4711_R1"
        version="3">
      <d2:situationRecordVersionTime>2026-05-01T08:30:00Z</d2:situationRecordVersionTime>
      ...
    </d2:situationRecord>
  </d2:situation>
</d2:payloadPublication>

Parse the SituationRecords, map fields to your normalized TrafficEvent, persist, and diff against the previous fetch to track lifecycle changes — new severity, new estimated end time, archival.

9. Where This Fits Inside NAPSPAN

Mobilithek is one of 30+ NAPs that NAPSPAN aggregates. The mTLS handshake described above is encapsulated in a single mobilithekClient that's reused across every adapter resource — events, AFIR EV charging, parking, weather. Adding a new Mobilithek publication is one line:

Register("mobilithek", "afir_charging", fetchMobilithekAFIR)

The fetch function uses the shared client, calls FetchPublication with the right Publikations-ID, hands the XML to the DATEX II parser, and returns normalized features. Same FetchFunc signature as the rest of the NAPs — the scheduler doesn't know or care that this one needs a TLS client cert.

Try It Without the Certs

If you want German motorway data without standing up your own Mobilithek account, that's exactly what NAPSPAN solves — we maintain the certificates, the parsers, and the lifecycle tracking, and serve the result through a normal API key over plain HTTPS:

curl "https://api.napspan.com/api/v1/events?country=DE&status=active" \
  -H "X-API-Key: your_key"

Mobilithek's mTLS flow is well-designed once you have the pieces in place — it's the lack of a working end-to-end walkthrough that makes the first integration painful. If this saved you a debugging afternoon, we'd love to hear about it.

Ready to try NAPSPAN?

Free 14-day trial. No certificates to manage. EU 27 + UK + EFTA, normalized.

Get Free API Key Explore the Map