> ## 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.

# External app magic link

> Email a customer a magic link that opens already authenticated in your app.

The external app magic link lets you email a customer a link that opens **already authenticated in your app** through a [Branch.io](https://www.branch.io) deep link, instead of routing them through the [Profile Portal](/experiences/profile-portal/overview) UI. Your backend triggers the call; the customer receives an email whose link is built on your app URL with a short-lived Omneo ID token and its expiry embedded.

This builds on the same profile-scoped token model as [Using Omneo ID](/dev-guides/frontend/omneo-id). Trigger the endpoint from a trusted server, never from the browser.

## How it works

<Steps>
  <Step title="Your backend calls the endpoint">
    Your server computes a shared-secret HMAC over the customer's email and calls `POST /api/auth/externalMagicLink` with a valid Omneo bearer token.
  </Step>

  <Step title="Omneo verifies the request">
    Omneo checks the bearer token, confirms the calling origin is allow-listed, and matches the request to your configured app entry using the supplied secret.
  </Step>

  <Step title="Omneo mints a token and emails the link">
    When the profile exists, Omneo mints a profile-scoped ID token, renders your link template with the token and expiry, and emails the link to the customer.
  </Step>

  <Step title="Your app validates the token">
    The customer opens the link in your app. Your app decodes the embedded token, checks the expiry, and uses it as a bearer token against the Omneo ID service.
  </Step>
</Steps>

## Endpoint

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

### Headers

| Header          | Required | Notes                                                             |
| --------------- | -------- | ----------------------------------------------------------------- |
| `Authorization` | Yes      | `Bearer <omneo-token>`. Validated against the Omneo auth service. |
| `Origin`        | Yes      | Your calling origin must be allow-listed by Omneo.                |

### Body

| Field      | Required | Description                                                                                                   |
| ---------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| `email`    | Yes      | Email of the profile to authenticate.                                                                         |
| `appUrl`   | Yes      | Your app base URL (origin). Must match a configured app entry.                                                |
| `secret`   | Yes      | `HMAC-SHA256` of the email, keyed with your shared secret. See [Computing the secret](#computing-the-secret). |
| `redirect` | No       | In-app redirect hint. Defaults to `/home`.                                                                    |

### Responses

| Status | Meaning                                                                                                                                                         |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`  | `{ data: { type: 'existing', id, email } }` when the profile exists; the link is emailed. `{ data: { type: 'new', email } }` when no profile matches the email. |
| `400`  | Missing `email`, `appUrl`, or `secret`.                                                                                                                         |
| `401`  | Missing `Authorization` header.                                                                                                                                 |
| `403`  | Invalid or expired bearer token, a disallowed `Origin`, or an `appUrl`/`secret` that does not match a configured app.                                           |
| `502`  | Token generation failed upstream.                                                                                                                               |

## Computing the secret

The bearer token alone does not authorise the call. You are also issued a **shared secret** and must prove you hold it on every request. Compute the value you send like this:

```text theme={null}
secret = HMAC-SHA256(message = email, key = sharedSecret)
```

Send the result as the `secret` body field. Omneo recomputes the same HMAC over the email it received and accepts the request only if the two match. Binding the HMAC to the email stops a captured `secret` from being replayed to request a link for a different profile.

Use the **exact** email string you send in the payload, with the same casing and trimming:

```bash theme={null}
node -e 'console.log(
  require("crypto").createHmac("sha256", "<shared-secret>")
    .update("user@example.com").digest("hex"))'
```

<Warning>
  Keep the shared secret on your server. It is the HMAC key and is never sent in the request body.
</Warning>

## Configuration

Your app entry is configured by Omneo during onboarding. You provide your app origin and a Branch.io link template, and Omneo issues you the shared secret. Each entry has three values:

| Value          | Description                                                                                          |
| -------------- | ---------------------------------------------------------------------------------------------------- |
| `baseUrl`      | The origin you send as `appUrl`. Requests are matched to an entry by comparing origins.              |
| `linkTemplate` | The full template used to build the emailed link. Omneo controls this, so you only send your origin. |
| `secret`       | The shared secret (HMAC key) issued to you.                                                          |

```json theme={null}
{
  "baseUrl": "https://shoesandsox.app.link",
  "linkTemplate": "https://shoesandsox.app.link/auth/callback?token={{token}}&iFrame=true&expiry={{expiry}}&redirect={{redirect}}",
  "secret": "<the-shared-secret-issued-to-you>"
}
```

The template is rendered with the variables `token`, `expiry`, and `redirect`. The example above renders to:

```text theme={null}
https://shoesandsox.app.link/auth/callback?token=<base64-jwt>&iFrame=true&expiry=<unix-seconds>&redirect=/home
```

Ensure the rendered `token` and `redirect` values are URL-encoded (or use a base64url-safe token) so characters like `+`, `/`, and `=` survive query-string parsing. Use double braces, for example `{{token}}`, in the template. A template with no placeholders falls back to appending `?token=...&expiry=...`.

## Validating the ID token on app open

The `token` query parameter is a base64-encoded JWT. When the customer opens the link, your app should:

1. Base64-decode the `token` value, then JWT-decode it.
2. Read the `pid` (profile ID) and `exp` (unix expiry) claims.
3. Reject the link if `exp` is in the past. Links are valid for about 24 hours.
4. Use the decoded JWT as a bearer token against the Omneo ID service, for example `GET https://api.[tenant].getomneo.com/id/api/v1/profiles/me`.

## Refreshing the token

The embedded ID token is short-lived. To obtain a fresh token, mint one **server-side** so the Omneo bearer token never reaches the client:

```http theme={null}
POST https://api.[tenant].getomneo.com/id/api/v1/auth/token
Authorization: Bearer <your-omneo-token>
Content-Type: application/json

{ "id": "<profile-id>" }
```

```json theme={null}
{ "data": { "token": "<jwt>" } }
```

This is the same profile-scoped token flow covered in [Using Omneo ID](/dev-guides/frontend/omneo-id). Hand the returned token to your app the same way you handle the token embedded in the magic link.

## Related

* [Using Omneo ID](/dev-guides/frontend/omneo-id)
* [Profile Portal overview](/experiences/profile-portal/overview)
* [Profile Portal concept](/concepts/platform-surfaces/profile-portal)
* [Passwords and authentication](/business-guides/profile-design/passwords)
