lacodix / membergy-sdk-php
PHP SDK for the Membergy API. Framework-agnostic with optional Laravel integration.
Requires
- php: ^8.3
- ext-json: *
- psr/simple-cache: ^3.0
- saloonphp/saloon: ^4.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.13
- larastan/larastan: ^3.1
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^4.0
- phpstan/phpstan: ^2.0
- saloonphp/laravel-plugin: ^4.0
Suggests
- illuminate/support: Required for the optional Laravel integration (ServiceProvider, Facade, config publishing).
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-05 06:02:49 UTC
README
PHP SDK for the Membergy API.
Framework-agnostic: works in any PHP 8.3+ project (Laravel, WordPress, Symfony, plain PHP). Ships with an optional Laravel integration (ServiceProvider, Facade, config publishing) that auto-activates when Laravel is present.
Status
Early development. The versioned CMS v1 slice and the binding S-1 through S-5 website-ready scope are implemented:
-
MembergyClient+MembergyConnector(Saloon-based) -
content()->pages()(list, find, dynamic includes) -
content()->posts()(list, category filter, find, dynamic includes) -
content()->postCategories() -
content()->menus()with recursive typed targets -
content()->images()/->files() -
content()->events()/->boilerplates() -
content()->search()(typed page, post and event groups with continuation) -
newsletter()(UUID categories, DOI subscribe / headless confirmation / signed unsubscribe request) - Typed BlockDocument v1 DTOs,
UnknownBlock,UnknownMenuTarget - Configurable
MenuUrlResolverfoundation -
auth()(credentials and two-factor token, current-token revocation, password reset) -
me()->access()(tenant access proof independent of person self-service) -
me()->person()(profile, dynamic form, updates) -
me()->newsletter()(member-owned category subscriptions) - Conditional public/member cache partitions, ETag revalidation and explicit purge
- Optional recursive Blade renderer, responsive image helpers and Laravel installer
- API/SDK compatibility matrix, bounded retries, timeouts and rate-limit exception
Requirements
- PHP 8.3+
- A Membergy instance with the public content API enabled for your tenant.
Installation
composer require lacodix/membergy-sdk-php
Usage (framework-agnostic)
use Lacodix\MembergySdk\Enums\EventVisibilityType; use Lacodix\MembergySdk\Enums\SearchResultType; use Lacodix\MembergySdk\MembergyClient; $client = new MembergyClient( baseUrl: 'https://membergy.app', tenant: 'my-club', cmsToken: $serverCmsToken, // Load from your server-side secret store. ); $page = $client->content()->pages()->find('willkommen'); foreach ($page->content->blocks as $block) { // Match concrete DTOs such as TitleBlock, TextBlock or ColumnsBlock. } $posts = $client->content()->posts() ->category('news') ->perPage(10) ->get(); $post = $client->content()->posts()->find('my-slug', includeDynamic: true); $hero = $client->image('11111111-1111-4111-8111-111111111111') ->width(1600) ->height(900) ->focus(0.52, 0.38) ->format('webp'); $heroUrl = $hero->url(); $heroSrcset = $hero->srcset([480, 768, 1280, 1600]); $events = $client->content()->events() ->eventType('performance') ->visibilities(EventVisibilityType::PUBLIC) ->perPage(20) ->get(); $search = $client->content()->search() ->query('Jahresbericht') ->get(); $morePosts = $client->content()->search() ->query('Jahresbericht') ->type(SearchResultType::POST) ->page(2) ->get(); $categories = $client->newsletter()->categories(); $client->newsletter()->subscribe('guest@example.org', [$categories[0]->uuid]); $client->newsletter()->confirm($opaqueConfirmationToken); $client->newsletter()->requestUnsubscribe('guest@example.org'); $client->withoutUserToken()->newsletter()->unsubscribeAll($opaqueUnsubscribeToken);
Posts use offset pagination by default; page(2)->perPage(10) keeps that behavior.
For newest-first cursor pagination (publication date, then ID, descending), opt in
explicitly, including on the first page:
$posts = $client->content()->posts()->category('news')->cursor()->perPage(10); $first = $posts->get(); if ($first->hasMorePages()) { $next = $posts->cursor($first->nextCursor())->get(); if ($next->previousCursor() !== null) { $previous = $posts->cursor($next->previousCursor())->get(); } }
cursor() and cursor(null) select the initial cursor page and clear any previous
cursor or page number. Non-null blank cursors are rejected; other cursors are opaque
and sent unchanged. Keep the same category and page size when following cursors.
Each chained call returns a new resource. Calling page(n) selects offset pagination
again and clears cursor settings.
Select multiple categories or exclude categories on the server before pagination:
$posts = $client->content()->posts() ->categories('news', 'reports') ->excludeCategories('archive') ->cursor() ->perPage(10); $first = $posts->get();
categories(string ...$slugs) uses OR inclusion. excludeCategories(string ...$slugs)
can accompany either inclusion method; exclusions win overlap. Exclusions alone retain
uncategorized posts. Each method requires 1–100 nonblank strings of at most 255 Unicode
characters and rejects named argument keys. Values are sent unchanged. Combining
category() with categories() throws InvalidArgumentException in either order;
empty lists and invalid bounds also fail locally before any request. Filters survive
page(), cursor() and cursor(null) chaining without mutating the base resource.
Cursor results contain the same PostSummary items. Use nextCursor(),
previousCursor() and hasMorePages() to navigate. meta contains per_page, path,
next_cursor and prev_cursor; links contains prev/next URLs and null
first/last. Offset totals and page numbers are unavailable in cursor mode:
currentPage(), lastPage() and total() return null.
The public subscribe and unsubscribe-request calls return a neutral status and never
expose whether the email address or subscription exists. By default, expiring signed
links sent by Membergy open its scanner-safe browser landing. A tenant-configured,
first-party landing may instead submit the opaque confirmation token from its explicit
POST handler to confirm() server-to-server. It may likewise submit an opaque full-
unsubscribe token to unsubscribeAll() from an explicit POST handler. The SDK does not
decode or cache these tokens, and neither method accepts a callback URL. A GET never
changes a subscription. Existing signed category-specific endpoints remain accepted by
Membergy for compatibility; new consumers intentionally use unsubscribeAll() for an
opaque guest credential and the authenticated me()->newsletter() self-service for
category changes.
Membergy has replaced its former forms/register endpoint with dedicated registration
offer and application endpoints. The SDK now provides provisional, typed
registrationOffers() and registrationApplications() resources against the captured
registration-v1 contract. See registration usage and limits.
Backend review and an exclusive serial backend test gate are still pending; this
contract is not yet integrated or guaranteed for consumers. Early 0.x package
releases may include this provisional implementation; they do not establish backend
acceptance. See package release policy.
Authenticated per-user access
Public content requires no auth. For members-only content (CMS
visibility = members), obtain a versioned user bearer token and switch the client:
$token = $client->auth()->tokenFromCredentials( 'member@example.org', 'secret-password', 'Club website', remember: false, // true: absolute 30 days; false: sliding two hours ); $authed = $client->withUserToken($token->accessToken); $access = $authed->me()->access()->get(); $internalPost = $authed->content()->posts()->find('internal-only-slug'); $memberEvents = $authed->content()->events() ->visibilities(EventVisibilityType::PUBLIC, EventVisibilityType::MEMBERS) ->perPage(20) ->get(); $profile = $authed->me()->person()->get(); $profileForm = $authed->me()->person()->form(); $updatedProfile = $authed->me()->person()->update([ 'firstname' => 'Alex', 'metadata' => ['shirt_size' => 'L'], ]); $subscriptions = $authed->me()->newsletter()->get(); $authed->me()->newsletter()->replace([ '11111111-1111-4111-8111-111111111111', ]); $authed->auth()->revokeCurrentToken();
The tenant access proof accepts direct single-/multi-tenant membership or global landlord
access and does not require a person record. AccessToken exposes emailVerified, remember
and loginEmail; protected endpoints still enforce verification and tenant permissions.
Tokens issued by auth-v1 have website scope: anonymous functions, member-visible CMS/search,
me() self-service, account-bound registration and self-revocation. Generic administration
APIs are refused even for admin/landlord accounts. Pre-deploy unmarked tokens return 401.
Normal tokens expire two hours after their last backend use; remembered tokens expire
absolutely 30 days after login. For normal tokens expiresAt is the issuance-time idle
horizon, not an immutable future deadline. The backend remains authoritative.
Confirmed two-factor authentication throws TwoFactorRequiredException with a random
challenge and expiresAt. Store only the challenge on the server until the second request:
// From a caught TwoFactorRequiredException; the challenge expires after five minutes. $token = $client->auth()->completeTwoFactor($challenge, code: $totpCode); // Alternatively, exactly one factor: $token = $client->auth()->completeTwoFactor($challenge, recoveryCode: $recoveryCode);
Five wrong attempts kill a challenge; success is single-use. Recovery codes are replaced and
TOTP replay is refused. Wrong and dead challenges expose stable RequestValidationException::errorCode
values; 429 maps to RateLimitException::retryAfterSeconds. The SDK clears any user bearer
for credential and two-factor issuance. The legacy raw response cannot bypass 2FA.
Own person reads, form and updates require password confirmation newer than three hours.
Catch PasswordConfirmationRequiredException, ask for the password on your website, then:
$result = $authed->me()->password()->confirm($password); // AuthResult::status = password_confirmed
Login after the full second factor and successful password changes refresh the token timestamp. Access, newsletter and password change do not require a separate fresh confirmation. Keep interrupted non-secret form values server-side and ask the user to save again afterward. All credential, challenge, code, recovery-code and confirmation parameters are sensitive; Connector-level Saloon request/response hooks stay silent when a user token is configured; request-level hooks also stay silent on bearer-authenticated sends. Sensitive operations suppress both levels even without a bearer. Anonymous non-sensitive debugging remains available, and object debugging redacts token/secret values. Never log request bodies or serialize secret objects.
Password reset reuses Membergy's existing Fortify password policy and notifications:
$client->auth()->requestPasswordResetLink('member@example.org'); $client->auth()->resetPassword( $resetToken, 'member@example.org', $newPassword, $newPassword, );
The backend retains its pre-existing raw-string application/json token response for old
consumers. The SDK always opts into the structured auth-v1 vendor media type.
Conditional caching and transport resilience
Framework-agnostic consumers can add any PSR-16 stores without changing the resource API:
use Lacodix\MembergySdk\Cache\CacheOptions; $cached = $client->withCache( publicStore: $publicPsr16Store, memberStore: $privatePerMemberPsr16Store, options: new CacheOptions( prefix: 'club-website', resourceTtls: ['events' => 60, 'pages' => 300], ), ); $page = $cached->content()->pages()->find('willkommen'); $cached->flush('pages'); // or flush() for every resource generation
Only successful JSON GET/HEAD responses with explicit compatible Cache-Control
directives are stored. Fresh entries avoid a request; stale entries revalidate with both
If-None-Match and If-Modified-Since, and a 304 restores the cached body.
Server no-store always wins. Public and member keys use separate stores/scopes;
both partitions also include a fingerprint of the Tenant CMS credential, so credentials
cannot share cached content. No raw credential is stored in cache keys or tags. Existing
cache entries become cold after this authentication change or a CMS credential rotation.
response keys are opaque hashes of the namespace and request identity, never raw URLs
or bearer tokens. Laravel leaves
member caching disabled by default.
Registration resources bypass SDK caching entirely, including private offers, prefill, token landings and document bytes. Resume credentials are supplied to each offer/submit request explicitly and never stored on the client or shared connector.
GET/HEAD requests retry transient network failures, 429 and 5xx with bounded exponential
backoff. Writes are never replayed. Exhausted 429 responses become
RateLimitException with an optional retryAfterSeconds value.
Registration requests make one attempt and use separate, sanitized
RegistrationException types, including RegistrationRateLimitException.
High-level non-registration resources convert Saloon HTTP and transport failures into
Lacodix\MembergySdk\Exceptions\MembergyException subtypes. Unmapped HTTP errors,
including exhausted 5xx reads and failed writes, throw HttpRequestException with a
numeric status and a fixed message. TransportException has a fixed message and no
response or HTTP status. These failures and the existing mapped exceptions have no
previous exception; they never chain Saloon/Guzzle request or response objects.
Existing authentication, two-factor, self-service, validation and rate-limit exception
types remain available. RequestValidationException::errors / errorCode and
TwoFactorRequiredException::challenge / expiresAt retain their documented values;
validation fields and challenges are redacted in PHP's native var_dump output.
Their public typed values can still appear in Symfony VarDumper, Laravel dump / dd,
var_export or serializers. Handle them explicitly; never log or serialize them.
Only parameters marked #[SensitiveParameter] (including Request objects, profile
value arrays and opaque newsletter tokens) are masked in default exception traces.
This replaces the earlier high-level Saloon exception behavior, including
ServiceUnavailableException; update consumer catches to the SDK exceptions.
Direct $client->connector()->send() is deliberately outside this boundary and keeps
raw Saloon exceptions, which can expose request headers, bodies and response data.
Do not dump those exceptions. Registration's separate exception boundary is unchanged.
Menus and consumer URLs
Membergy returns stable target type, UUID, slug and title. Route decisions stay in the consuming website:
use Lacodix\MembergySdk\MenuUrlResolver; $resolver = new MenuUrlResolver([ 'page' => fn ($target) => '/'.$target->slug, 'post' => fn ($target) => '/news/'.$target->slug, 'post_category' => fn ($target) => '/news/category/'.$target->slug, ]); $menu = $client->content()->menus()->find('main'); $url = $resolver->resolve($menu->items[0]->target);
Custom links pass through unchanged. Unknown future block and target types are retained as raw fallbacks so additive API evolution does not break older SDK versions.
The original $client is untouched — withUserToken() returns a new
instance, so it's safe to use in Octane / long-lived processes.
Usage (Laravel)
Install config and overridable renderer views:
php artisan membergy:install
Add to .env:
MEMBERGY_URL=https://membergy.app
MEMBERGY_TENANT=my-club
MEMBERGY_CMS_TOKEN=<tenant-cms-credential-from-secret-store>
MEMBERGY_URL defaults to https://membergy.app. Point it at the reachable
local or review instance when developing a consumer application, for example
MEMBERGY_URL=http://membergy.test.
MEMBERGY_CMS_TOKEN is required for every SDK transport call, including guest login,
password, newsletter, contact and registration calls. It is a Tenant-owned credential
with fixed cms scope, carried in X-Membergy-Cms-Token; it is independent of the
optional visitor Authorization bearer and survives withoutUserToken() and all client
copies. Missing or blank configuration throws CmsAuthenticationException locally before
network or response-cache access. Existing positional constructor arguments keep their
positions; append the named cmsToken argument. fromConnector() uses that connector's
credential; supplying another cmsToken alongside an injected connector is rejected.
HTTP 401 with top-level code cms_client_unauthenticated also throws
CmsAuthenticationException, independently of visitor authentication. Existing visitor
401 codes, including cms_unauthenticated, keep their authentication exceptions; a rejected
CMS credential must not expire an otherwise valid visitor session.
Set visitor identity through the constructor or immutable user-token copy methods.
Replacing Authorization through inherited Saloon authenticators/headers or PSR hooks
is rejected, so a request cannot reuse another visitor's private cache partition.
Keep the CMS credential server-side. Never place it in browser configuration, media URLs, HTML or logs. The exact browser binary image/file routes remain key-exempt with their existing visibility/revision checks; JSON metadata calls require the key. URL builders perform no transport and remain usable without a key. No media proxy is introduced.
SDK transport accepts only the configured trusted HTTP(S) origin, with redirects and raw
sender diagnostics disabled. SDK debug callbacks are suppressed for all requests while
their existing die stop timing is retained. Client serialization is rejected. CMS
headers are applied only to the outgoing PSR request and removed from retained response
request metadata; reflected credentials are redacted from responses before errors/cache.
The SDK uses its own Guzzle sender instead of global Laravel event/telemetry senders;
raw debug, on_stats and on_headers sender callbacks are disabled. Do not log low-level
transport objects or manually inspect/send credential-bearing outgoing PSR requests.
The current credential-gated backend returns content data/actions private, no-store
without validators. The SDK honors that directive. Its conditional cache machinery stays
available for compatible responses; a fresh cached entry cannot detect server-side
revocation until revalidation/expiry, and authoritative 401/403 always replace stale data.
Then resolve the client from the container or use the facade:
use Lacodix\MembergySdk\Laravel\Facades\Membergy; $posts = Membergy::content()->posts()->get();
The Laravel binding is request-scoped. A token in the configured session key takes
precedence over the optional static token without leaking into Octane or later requests.
Config-sourced static tokens remain available for CMS calls; registration offer GET and
submit reject them before sending. Use withoutUserToken() for guests or the current
applicant's session / explicit withUserToken() client for account registration.
Public caching uses the configured Laravel cache store. Useful commands:
php artisan membergy:cache-clear php artisan membergy:cache-clear pages
Optional Blade renderer
The renderer covers every BlockDocument-v1 block, recursive columns, common settings and
dynamic post-category includes. Unknown future blocks are skipped. Views live under the
membergy:: namespace and can be published/overridden without forking the SDK:
<x-membergy::content :document="$page->content" :included="$page->included" /> <x-membergy::menu :menu="$menu" :resolve="$menuUrlResolver" /> <x-membergy::events :events="$events->items" />
The SDK provides semantic membergy-* classes but deliberately no fixed theme.
Renderer views trust only the backend-sanitized rich-text/raw-HTML contract; unknown raw
payloads are never rendered.
Each anchor_navigation v1 block can set data.scope to page (all supported
blocks with a nonblank common settings.anchor, except this marker) or following
(strictly after this marker until strictly before the next navigation marker, or
document end). Omitted scope defaults to page. Every marker ends the preceding
following range, irrespective of scope. Ordering is container-first, left-to-right,
depth-first through columns, including the columns container itself. Other markers'
anchors are eligible in page scope. Original anchor identities and duplicates are
preserved; rich-text HTML IDs are not collected. Optional supplied levels remain
typed inert metadata; absence does not manufacture levels. Empty ranges render an
empty menu. Labels use existing plain heading/title text when nonblank, otherwise
the anchor; Blade escapes labels and anchor attributes. No entries/count wire API
is introduced. AnchorNavigationBlock::dataToArray() exports scope, supplied levels
and additive data fields.
render(document) supplies the full document context. Existing three-argument
renderBlocks() / renderBlock() calls remain supported: page menus use the
caller-supplied anchors; renderBlocks() derives following ranges from its supplied
block list. A standalone renderBlock() cannot infer surrounding document order,
so its supplied anchors are treated as the caller's selected range. The supplied renderer carries an immutable navigation context through recursive
columns, including older overrides using three arguments. The optional fourth
navigation-range map can still select an explicit context. Use
render(document) for whole-document selection and own-marker exclusion.
contact_form v1 carries data.form_uuid (UUID string or explicit null).
ContactFormBlock retains main's original formHandle/topics constructor positions
and legacy hydration, alongside native formUuid, definitionVersion, submitLabel
and title. Invalid supplied UUIDs still fail hydration. The default view is an inert
UUID hook; applications can fetch native definitions and submit through
$client->content()->forms() as described in contact forms.
Published application views may render their own native forms.
table v1 exposes title, plain-text headers, rectangular rows, and hasHeader.
The optional wire has_header must agree with the headers; older responses derive it.
The default view escapes every cell, preserves empty cells/rows, and emits column
headers only when present. Tables accept at most 12 columns, 101 logical rows including
the header, and 2000 characters per cell.
Page/Post, their summaries and search hits expose hasMemberContinuation (also retained in extra['has_member_continuation'] for existing consumers; false when absent on
older responses). The backend supplies guest/member projections and removes editorial
member_continuation markers. The SDK never fetches or reconstructs private continuation
content. Markers hydrate to an inert DTO and produce neither markup nor navigation links.
Testimonials retain items[].name and expose optional description:string|null,
escaped separately from the backend-sanitized rich text. Missing media/file
selections in unfinished blocks hydrate as null; views omit missing media and
file links. Unselected post categories retain null or the empty string.
The current CMS registry has 17 v1 block types. The fixtures and schema mirrors are synchronized with the current backend public contract; unknown block versions/types and additive fields retain their existing fallback behavior. This source compatibility does not establish a released or deployed consumer integration.
Upgrading an application with published/custom Blade views requires manual
reconciliation: files under resources/views/vendor/membergy/blocks/ override the
package's vendor/lacodix/membergy-sdk-php/resources/views/blocks/ files, so a Composer
upgrade alone leaves old overrides active. Back up/version your overrides, compare
each affected file with the new package view, then merge the following changes into
your custom markup:
anchor-navigation.blade.php: consume the renderer-selected$anchorsdirectly; remove the oldlevelsfilter and keep escaped link labels/attributes.columns.blade.php: existing three-argument$renderer->renderBlocks()calls retain whole-document context automatically when invoked through the supplied renderer. Explicit fourth-argument navigation maps remain supported.contact-form.blade.php: use nullableformUuidfor native forms. ExistingformHandle/topicsintegrations remain available. Keep browser forms behind the application's own CSRF and server-side native SDK gateway; see the contact guide.image.blade.php,text-image.blade.php,gallery.blade.php,logo-cloud.blade.php,video.blade.php,downloads.blade.php: guard nullable image/video/file references before accessing their UUID or building URLs; retain text independently where applicable. In image/gallery/logo-cloud andagenda.blade.php, guard nullable/empty link URLs before rendering an anchor.testimonials.blade.php: retainnameand sanitized quote output, and render the optional description with escaped Blade output only when nonnull.
For an uncustomized override, compare it first and deliberately replace that one
file with the matching package view. Publishing with --tag=membergy-views without
--force can add missing files but does not upgrade existing overrides. Do not
blindly force-republish the directory or overwrite custom views. After merging,
clear compiled views with php artisan view:clear in the consumer's normal deployment
workflow. Smoke-test page/following menus across columns, empty media selections,
Form UUID/null hooks and testimonial escaping in that consumer. These instructions
do not claim execution against any published/custom consumer installation.
See the compatibility matrix for the supported API and contract versions.
Testing
MEMBERGY_BACKEND_ROOT=/path/to/cms-source \
MEMBERGY_REGISTRATION_BACKEND_ROOT=/path/to/registration-source \
composer test
Unit tests run locally. Integration tests use Saloon's MockClient
to simulate HTTP responses — no running Membergy instance required. Both source
checkouts are required for the separate fixture gates.
The fixtures under tests/Fixtures/Contracts/Cms/v1 contain all 64 current backend
CMS JSON fixtures (responses and document examples), plus block-registry.json.
The gate requires the real MEMBERGY_BACKEND_ROOT checkout and compares complete
path sets and every fixture byte. The schema family is mirrored under the existing
tests/Fixtures/Contracts/AnchorNavigation/v1 destination and checked independently
against backend schema bytes. Local synthetic anchor examples remain distinct from
backend HTTP captures. Registration fixtures and drift remain independently gated.
Registration fixtures live separately under tests/Fixtures/Contracts/Registration/v1:
43 captured backend responses (including the exact PDF bytes), their request/status/header
metadata, OpenAPI, JSON schema and provenance. MEMBERGY_REGISTRATION_BACKEND_ROOT
selects that source checkout. Its independent gate rejects missing or extra SDK files
and any byte difference; it never skips or changes the CMS gate's source.
License
MIT — see LICENSE.md.
Own contact rows and bank details
me()->person()->form() exposes repeater fields with typed child fields and max.
$form->repeaterRows('contact_details') returns FormRow objects (key, values).
The raw keyed values remain available unchanged. PersonProfile exposes typed contactDetails
and bankDetails; the authorized owner receives their full own IBAN. Never cache it across users.
use Lacodix\MembergySdk\DataObjects\ContactDetail; $client->me()->person()->update([ // Include the other required values from the current form. 'contact_details' => [ new ContactDetail('78001', 'private_email', 'member@example.test', 'club'), new ContactDetail($clientUuid, 'private_phone', '+513512345678', 'private'), ], 'bank_iban' => 'DE89370400440532013000', ]);
Existing keys are opaque IDs of the owner's rows; new keys must be client-generated UUIDs.
Submitting the repeater replaces all rows (missing existing rows are deleted). An empty array
submits an empty object and clears contacts; omitting the repeater preserves them. Foreign IDs,
unknown row fields and invalid contact data are rejected atomically. Contact emails never change
the login email. RequestValidationException::errors retains dotted row paths.
Authenticated password change and management access
$result = $client->me()->password()->update($currentPassword, $password, $confirmation); // PasswordUpdateResult::status === 'password_updated' $managementUrl = $client->me()->access()->get()->managementUrl; // string|null
Password parameters carry #[SensitiveParameter]; the password request suppresses both
request and connector Saloon debugging and redacts object debug output. Do not log or dump
request bodies. Validation maps to RequestValidationException::errors, and throttling to
RateLimitException::retryAfterSeconds. All tokens/sessions remain valid after a change;
Membergy sends no mail. The new endpoint requires the additive self-service-v1 backend.
Only real management rights in the URL tenant yield a management URL. The destination
enters Membergy through its own login and permission checks.
One-time management login (package E, 2026-10-09)
$client->me()->managementLogin() returns ManagementLogin with a sensitive url and typed
expiresAt. Its URL and payload parameters are SensitiveParameter and its __debugInfo redacts
the URL. The request uses self-service-v1; 403/401/429 map to the existing forbidden/auth/rate
exceptions. This method needs the additive backend management-login route and ticket migration.
64 CMS fixtures plus the BlockRegistry and the independent registration provenance
are synced byte-for-byte. Capture and canonicalization remain backend-owned.
The server enforces a 60-second single-use ticket bound to user, club and issuing website token. Real management rights, token lifetime and confirmed 2FA activation are checked again on redeem. The new web session inherits the token confirmation time, preserving the three-hour password gate. It replaces any existing Membergy session, including impersonation, and has one fixed club management target. Website and Membergy logouts stay independent. Treat the URL as a credential; never print it or enable raw response debugging. Configure upstream access logs to mask ticket paths.