rathauslotse/bayernportal-api
A Laravel/PHP client for the BayernPortal API, offering convenient access to Bavarian administrative services and municipal portal data. It simplifies requests, authentication, and response handling so you can integrate BayernPortal content into your applications.
This package wraps the BayernPortal API — the Bavarian state portal that exposes administrative services (Leistungen), responsible authorities (Organisationen), and life situations (Lebenslagen). It is the package you reach for when a municipal or public-sector Laravel app needs to display "which office handles X" or "what do I need to bring for a Personalausweis" without re-keying that data by hand.
Minimal setup
composer require rathauslotse/bayernportal-api
php artisan vendor:publish --tag=bayernportal-api-config
Add the credentials to .env (the BayernPortal API requires a registered key/token — request one through the portal's developer onboarding before first use):
BAYERNPORTAL_API_URL=https://www.bayernportal.de/api
BAYERNPORTAL_API_KEY=your-token
The service provider is auto-discovered; the config file is where base URL, timeout, and credentials live, so read that file first — it is the most reliable map of what the package actually expects.
Where to look first
config/bayernportal.php — endpoints, credentials, retry/timeout knobs.Rathauslotse\BayernPortal\Facades\BayernPortal or a BayernPortalClient binding) — this is the public API surface.src/ grouped by domain (Leistungen, Organisationen, Lebenslagen).First use case
The most common day-one task is: "Show the BayernPortal entry for a service, with its description and the online form link."
use Rathauslotse\BayernPortal\Facades\BayernPortal;
$leistung = BayernPortal::leistungen()->find(1024);
return view('services.show', [
'name' => $leistung->name,
'description' => $leistung->description,
'onlineUrl' => $leistung->onlinedienstLink,
]);
If the exact accessor names differ in the source, the shape of the API call is still the same — you are asking the wrapper to proxy a REST resource and hydrate a result object.
Wrap reads in a cached repository. BayernPortal data changes on the timescale of legislation, not requests. Put a thin repository between your controllers and the facade and cache aggressively:
class ServiceDirectory
{
public function find(int $id): ?Leistung
{
return Cache::remember("bayernportal.leistung.{$id}", now()->addDay(), fn () =>
BayernPortal::leistungen()->find($id)
);
}
}
Bind this in a service provider so tests can swap it for a fake without touching the package.
Search-driven public pages. The API supports free-text search over services and life situations, which is exactly what you want behind a "Was möchten Sie erledigen?" search box. Debounce the client, call the package from a small controller or Livewire component, and cache the top queries — those are the ones that get hit hardest.
Local sync for offline resilience. For portals that cannot afford an outage when BayernPortal is slow, run a scheduled Artisan command that pulls the catalogue into Eloquent models and serves the site from your own tables:
Schedule::command('bayernportal:sync')->dailyAt('03:00');
Wrap the sync in ->withoutOverlapping() and chunk the fetch loop; the full catalogue is large enough that a single request will not carry it.
Compose cross-resource detail pages. A Leistung points at one or more Organisationen. The typical page is a three-call composition — fetch the service, resolve its authority IDs, then render opening hours and contact details. Fire those secondary lookups in a Concurrency/parallel HTTP group where possible, or preload them from your synced tables.
Use the package inside queued jobs, not controllers. Bulk hydration, sitemap generation, and nightly sync all belong in jobs with $tries and backoff() set. The API is a government service; respect it.
Keep a translation layer. The package returns German field names and German content. If your UI is bilingual or your domain model uses English, map to your own DTOs immediately after the call rather than leaking $leistung->zuständigeStelle into views.
v1.0.0 is a fresh tag. The changelog link is the only artefact, so pin the constraint ("rathauslotse/bayernportal-api": "^1.0") and read the diff of any 1.x bump before deploying. Treat the public class names as stable from here on, but verify accessor names against the source rather than assuming them from the README.
Credentials are not optional. Unauthenticated calls will fail — check that BAYERNPORTAL_API_KEY is set in the target environment before debugging your own code. php artisan config:cache after changing .env, or you will chase a stale-config ghost.
German content means German encodings. Umlauts and ß must survive your database column collation (utf8mb4), your JSON responses, and your PDF/exports. Test with a Leistung that has an umlaut in its name.
Do not let the remote API dictate your uptime. Add a circuit breaker or at minimum a fallback: serve the last-known-good cached payload when the call throws. A municipal site that 500s because a state API is down is a bad look.
Handle the exception types explicitly. Distinguish "not found" (render a 404) from transport failures (log, serve cache, alert). Blanket catch (\Throwable) will hide a rotated API key for weeks.
Retries need jitter and a cap. If the package exposes the underlying Laravel HTTP client, configure retry(3, 200, throw: false) and a 5–10 s timeout. Do not retry a 404.
Testing is trivial — use Http::fake(). Because the wrapper sits on Laravel's HTTP client, you can fake the recorded fixture responses and assert your mapping layer without a sandbox account. Build a handful of fixtures (one Leistung, one Organisation, one search result) and reuse them everywhere.
Pagination is the usual trap. Lists return paged; the first page looks complete and is not. Assert on the pagination metadata and loop until exhausted, with a hard iteration ceiling so a misbehaving API cannot spin forever.
Extension points to reach for instead of forking. The client is resolved from the container and the config is published — bind your own subclassed client or resource wrapper via $this->app->bind() when you need extra logging, header injection, or a mock. Only patch the vendor tree if a genuine bug blocks you; upstream it as an issue with the changelog link.
Log the correlation ID. Public-sector APIs often return a request identifier on error responses. Capture it in your logging context; the portal's support team will ask for it and you will not want to reconstruct it from timestamps.
How can I help you explore Laravel packages today?