Email a customer a portal sign-in link
The portal.login_links.request operation: POST /api/v1/public/{brandSlug}/portal/login_links in the REST API and the portal_login_links_request tool in the MCP server.
/api/v1/public/{brandSlug}/portal/login_linksEmails a one-time sign-in link for the brand's customer portal to the address, if it belongs to a customer of the brand in the portal's mode. The link works once, for 15 minutes; only a hash of its token is stored.
The answer is the same whether or not the address belongs to a customer, so nobody can find out who the
brand's customers are. Requests are limited per address (5 per 15 minutes per brand) and per client IP (20 per
hour), whether or not the address is known (rate_limited).
Public: the portal's sign-in page calls it, no API key needed.
At a glance
- REST:
POST /api/v1/public/{brandSlug}/portal/login_links - MCP tool:
portal_login_links_request - Operation ID:
portal.login_links.request - Scope: Brand
- Permission:
portal:write - Risk tier:
write - Dry run: Not supported
- Idempotency:
Idempotency-Keyrequired - Modes: Test and live
- Authentication: None (a public endpoint, rate-limited per IP address)
- Since: M9
Request
Path parameters
| Field | Type | Description |
|---|---|---|
brandSlug | string | The brand's slug, as in /portal/{brandSlug}. |
Body
A JSON object (Content-Type: application/json):
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The address the customer uses with the brand. |
Response
200 OK with a JSON object:
| Field | Type | Description |
|---|---|---|
object | "portal_login_link_request" | |
email | string | |
message | string | The same answer whether or not the address belongs to a customer (nothing is revealed). |
Examples
Ask for a sign-in link
POST /api/v1/public/genno/portal/login_links
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json
{
"email": "ana@example.com"
}
Response (200):
{
"object": "portal_login_link_request",
"email": "ana@example.com",
"message": "If ana@example.com belongs to an account with Genno, we've sent it a sign-in link. The link works once, for 15 minutes."
}
Errors
| Code | HTTP status | When | What to do |
|---|---|---|---|
validation_failed | 400 | email isn't an email address, or brandSlug isn't a slug. | Fix the fields listed in details.issues and send the request again. |
resource_not_found | 404 | No brand with this slug has a customer portal (draft and suspended brands have none). | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode. |
rate_limited | 429 | Too many sign-in links were asked for this address (5 per 15 minutes per brand) or from this client IP (20 per hour); wait retry_after_ms. | Wait for retry_after_ms and retry with backoff. |
It can also return these common errors: api_version_unsupported, invalid_json, unsupported_media_type, scope_required, idempotency_key_required, idempotency_key_reused, idempotency_in_progress, dry_run_not_supported, internal_error.