> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omneo.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Profile Portal login API

> Send a magic link or a one-time login code to a customer from your own site, then exchange the code for an Omneo ID token.

Profile Portal exposes the login endpoints its own pages use, so a brand website or app that owns the login form can trigger the same magic link email, email code, or SMS code and finish the login itself. The customer never has to see a Profile Portal page unless you want them to.

This is different from the [External app magic link](/dev-guides/frontend/external-app-magic-link), which builds the emailed link on your own app URL and needs a bearer token and a shared secret. The endpoints on this page are the public ones behind the Profile Portal login screen: no credentials, but rate limited and, for browser calls, restricted to allow-listed origins.

## Before you start

| Requirement | Detail |
| - | - |
| Base URL | Your Profile Portal domain, for example `https://profile.[brand].com.au`. Every path below is relative to it. |
| Calling from a browser | Your site's origin must be allow-listed by Omneo. Requests from any other origin receive `403`. Ask your Omneo account manager to add the origin. |
| Calling from a server | No allow-listing and no credentials are needed. |
| Rate limit | 5 requests per minute per IP address, per endpoint. Beyond that the endpoint returns `429` with a message you can show the customer. |
| Existing profiles only | Every request endpoint answers `type: "new"` when no profile matches, and sends nothing. Send those customers to your sign-up flow. |
| Tenant configuration | Email codes need email code login configured for your tenant, and SMS codes need SMS login configured. See [Configuring Profile Portal](/experiences/profile-portal/configuration#login-methods). Magic links need no extra setup. |

## How the code flow works

<Steps>
  <Step title="Your site requests a code">
    The customer enters their email address or mobile number on your site. Your site calls `POST /api/auth/email-code` or `POST /api/auth/sms`.
  </Step>

  <Step title="Omneo sends the code">
    When the profile exists, Omneo issues a 6-digit code and sends it using your configured email template or SMS sender. Nothing is sent for an unknown contact.
  </Step>

  <Step title="Your site verifies the code">
    The customer enters the code on your site. Your site calls `POST /api/auth/code/verify` with the code and the same email address or mobile number.
  </Step>

  <Step title="Omneo returns an ID token">
    Omneo checks the code, invalidates it, and returns a profile-scoped Omneo ID token. Use it to open Profile Portal already logged in, or call the Omneo ID service directly. See [Use the token](#use-the-token).
  </Step>
</Steps>

## Send a magic link

```http theme={null}
POST /api/auth
```

Emails the customer a magic link that opens Profile Portal already logged in. The link is valid for about 24 hours.

### Body

| Field | Required | Description |
| - | - | - |
| `email` | Yes | Email address of the profile. |
| `redirect` | No | Profile Portal path to open after login. Defaults to `/home`. |

Other fields accepted by this endpoint are used by Profile Portal's own pages and are not needed from an external site.

### Responses

| Status | Meaning |
| - | - |
| `200` | `{ data: { type: 'existing', id, email, identity, joined_at, first_name } }` when the profile exists; the link is emailed. `{ data: { type: 'new', email } }` when no profile matches. |
| `400` | Missing `email`. |
| `429` | Rate limit exceeded. |

`identity` is the customer's loyalty card number when your tenant has a loyalty card identity configured, otherwise `false`.

## Send a login code by email

```http theme={null}
POST /api/auth/email-code
```

### Body

| Field | Required | Description |
| - | - | - |
| `email` | Yes | Email address of the profile. Matching ignores case and surrounding whitespace. |

### Responses

| Status | Meaning |
| - | - |
| `200` | `{ data: { type: 'existing', id, email, identity, first_name } }` when the profile exists. A code is emailed, unless one was already sent inside the resend wait, in which case the earlier code stays valid and nothing new is sent. `{ data: { type: 'new', email } }` when no profile matches; no code is issued. |
| `400` | Missing `email`. |
| `429` | Rate limit exceeded. |
| `500` | The code could not be sent. The usual cause is that email code login is not configured for the tenant. |

## Send a login code by SMS

```http theme={null}
POST /api/auth/sms
```

### Body

| Field | Required | Description |
| - | - | - |
| `mobile_phone` | Yes | Mobile number as stored on the profile. |

### Responses

| Status | Meaning |
| - | - |
| `200` | `{ data: { type: 'existing', mobile_phone, identity } }` when the profile exists and a code is sent (or an earlier code is still inside the resend wait). `{ data: { type: 'new', mobile_phone } }` when no profile matches; no code is issued. |
| `400` | Missing `mobile_phone`. |
| `429` | Rate limit exceeded. |
| `500` | The SMS could not be sent. |

## Verify a code

```http theme={null}
POST /api/auth/code/verify
```

Verifies a code from either channel. Send the same identifier the code was requested with.

### Body

| Field | Required | Description |
| - | - | - |
| `code` | Yes | The 6-digit code the customer entered. |
| `email` | One of | The email address the code was sent to. |
| `mobile_phone` | One of | The mobile number the code was sent to. |

### Responses

| Status | Meaning |
| - | - |
| `200` | `{ data: { token, id } }`. `token` is a base64-encoded Omneo ID JWT for the profile and `id` is the profile ID. The code is invalidated. |
| `400` | Missing fields, or the code is wrong, expired, already used, or was sent to a different email address or mobile number. The response is the same in all of these cases, so show the customer one generic message. |
| `429` | Rate limit exceeded. |
| `500` | The code was accepted but a token could not be issued. The code is spent, so ask the customer to request a new one. |

`POST /api/auth/sms/retrieve` is a legacy alias of this endpoint with the same body and responses. Use `/api/auth/code/verify` for new integrations.

## Use the token

The `token` returned by verify is the same value Profile Portal puts in its own magic links, so you have two options.

**Open Profile Portal logged in.** Redirect the customer to the portal's login route with the token and an optional path:

```text theme={null}
https://profile.[brand].com.au/login?token=<token>&redirect=/home
```

**Stay on your site.** Base64-decode the token to get the JWT, read its `pid` (profile ID) and `exp` (unix expiry) claims, and use the JWT as a bearer token against the Omneo ID service, for example `GET https://api.[tenant].getomneo.com/id/api/v1/profiles/me`. The steps are the same as [validating the ID token on app open](/dev-guides/frontend/external-app-magic-link#validating-the-id-token-on-app-open). The token is short-lived; to refresh it, mint a new one server-side as described in [Using Omneo ID](/dev-guides/frontend/omneo-id).

## Code behaviour

| Rule | Value |
| - | - |
| Format | 6 digits |
| Lifetime | 15 minutes from issue |
| Uses | One. The code is invalidated when it is verified. |
| Active codes | One per email address or mobile number. Requesting a new code after the resend wait replaces the previous one. |
| Resend wait | 60 seconds by default, configurable per tenant. A request inside the wait returns `200` without sending a new code. |
| Wrong attempts | 5. The code is invalidated on the fifth wrong attempt and the customer must request a new one. |
| Channels | A code sent by email cannot be verified with a mobile number, and the reverse. |

## Example

```shell theme={null}
# Request a code by email
curl -s -X POST https://profile.[brand].com.au/api/auth/email-code \
  -H 'Content-Type: application/json' \
  -d '{"email":"customer@example.com"}'

# Verify it
curl -s -X POST https://profile.[brand].com.au/api/auth/code/verify \
  -H 'Content-Type: application/json' \
  -d '{"email":"customer@example.com","code":"123456"}'
```

The first call returns `type: "existing"`, the second returns `{ "data": { "token": "<base64-jwt>", "id": "<profile-id>" } }`, and repeating the second call returns `400` because the code has been used.

## Related

* [External app magic link](/dev-guides/frontend/external-app-magic-link)
* [Using Omneo ID](/dev-guides/frontend/omneo-id)
* [Configuring Profile Portal](/experiences/profile-portal/configuration)
* [Profile Portal overview](/experiences/profile-portal/overview)
* [Passwords and authentication](/business-guides/profile-design/passwords)
