# New device login alert: template and code

Source: https://www.courier.com/recipes/new-device-login-alert (manifest: https://www.courier.com/recipes/new-device-login-alert/manifest.json)

Tell people right away when their account is used from a new device.

A new device login alert warns users when their account signs in from a device you haven't seen before. It sends an email and an Inbox message at the same time, showing the device, location, and time, with a button to secure the account. Each new sign-in sends one alert, so there's nothing to stop.

Tested 2026-10-19: passing.

## When to send a new device login alert

| Step | When | Channels | Subject |
| --- | --- | --- | --- |
| 1 | When an account signs in from a new device | Email, Inbox | New sign-in to your Acme account |

## Copy

### New device sign-in alert (email)

Subject: "New sign-in to your Acme account", "New sign-in from {{device}}", "Was this you? New sign-in to Acme"

Your Acme account was used to sign in from a new device.

Device: {{device}}

Location: {{location}}

Time: {{datetime-format signed_in_at "MMM d, yyyy, h:mm a zzz" (default profile.timezone "America/New_York")}}

If this was you, there's nothing to do.

Button: Wasn't you? Secure your account (https://app.acme.com/security)

### New device sign-in alert (Inbox)

Subject: "New sign-in from {{device}}"

{{location}}, {{datetime-format signed_in_at "MMM d, yyyy, h:mm a zzz" (default profile.timezone "America/New_York")}}. If this wasn't you, secure your account.

Button: Wasn't you? Secure your account (https://app.acme.com/security)

## Create it in your workspace

```bash
COURIER_API_KEY=pk_... node --experimental-strip-types setup.ts
```

setup.ts

```typescript
// Creates this recipe's templates in your workspace from the templates/
// folder and publishes them. Run it once per workspace:
//
//   COURIER_API_KEY=pk_... node --experimental-strip-types setup.ts
//
// It prints the template IDs that start.ts needs.
import Courier from "@trycourier/courier";
import { readFileSync, readdirSync } from "node:fs";

const client = new Courier();
const here = new URL(".", import.meta.url);

const templateIds: Record<string, string> = {};
for (const file of readdirSync(new URL("templates/", here))) {
  const body = JSON.parse(
    readFileSync(new URL(`templates/${file}`, here), "utf8")
  );
  const template = await client.notifications.create(body);
  templateIds[file.replace(/\.json$/, "")] = template.id;
}

console.log(JSON.stringify({ templateIds }));
```

## Send it when a new device signs in

```typescript
// Alert someone by email and in your app's Inbox when their account is used
// from a new device, with the template ID setup.ts printed. Routing method
// "all" sends on every channel in the list instead of picking one. The templates
// format signedInAt as a date and time.
import Courier from "@trycourier/courier";

const client = new Courier();

export async function sendNewDeviceAlert(
  templateId: string,
  userId: string,
  device: string,
  location: string,
  signedInAt: Date
) {
  const { requestId } = await client.send.message({
    message: {
      to: { user_id: userId },
      template: templateId,
      data: {
        device,
        location,
        signed_in_at: signedInAt.toISOString(),
      },
      routing: { method: "all", channels: ["email", "inbox"] },
    },
  });
  return requestId;
}
```

```python
# Alert someone by email and in your app's Inbox when their account is used
# from a new device, with the template ID setup.ts printed. Routing method
# "all" sends on every channel in the list instead of picking one. The templates
# format signed_in_at as a date and time.
from datetime import datetime

from courier import Courier

client = Courier()


def send_new_device_alert(
    template_id: str,
    user_id: str,
    device: str,
    location: str,
    signed_in_at: datetime,
) -> str:
    response = client.send.message(
        message={
            "to": {"user_id": user_id},
            "template": template_id,
            "data": {
                "device": device,
                "location": location,
                "signed_in_at": signed_in_at.isoformat(),
            },
            "routing": {"method": "all", "channels": ["email", "inbox"]},
        },
    )
    return response.request_id
```

## Templates

setup.ts reads these from a templates/ folder next to it.

templates/new-device-alert.json

```json
{
  "notification": {
    "name": "New device sign-in alert",
    "tags": ["recipe", "new-device-login-alert"],
    "brand": null,
    "subscription": null,
    "routing": null,
    "content": {
      "version": "2022-01-01",
      "elements": [
        {
          "type": "channel",
          "channel": "email",
          "elements": [
            {
              "type": "meta",
              "title": "New sign-in to your Acme account"
            },
            {
              "type": "text",
              "content": "Your Acme account was used to sign in from a new device."
            },
            {
              "type": "text",
              "content": "Device: {{device}}"
            },
            {
              "type": "text",
              "content": "Location: {{location}}"
            },
            {
              "type": "text",
              "content": "Time: {{datetime-format signed_in_at \"MMM d, yyyy, h:mm a zzz\" (default profile.timezone \"America/New_York\")}}"
            },
            {
              "type": "action",
              "content": "Wasn't you? Secure your account",
              "href": "https://app.acme.com/security"
            },
            {
              "type": "text",
              "content": "If this was you, there's nothing to do."
            }
          ]
        },
        {
          "type": "channel",
          "channel": "inbox",
          "elements": [
            {
              "type": "meta",
              "title": "New sign-in from {{device}}"
            },
            {
              "type": "text",
              "content": "{{location}}, {{datetime-format signed_in_at \"MMM d, yyyy, h:mm a zzz\" (default profile.timezone \"America/New_York\")}}. If this wasn't you, secure your account."
            },
            {
              "type": "action",
              "content": "Wasn't you? Secure your account",
              "href": "https://app.acme.com/security"
            }
          ]
        }
      ]
    }
  },
  "state": "PUBLISHED"
}
```

## Hand it to your agent

```text
Add new device login alerts to this app, sent through Courier: an email and an Inbox message at the same time, with the device, location, and time and a Wasn't you? Secure your account button. Follow the steps at https://www.courier.com/recipes/new-device-login-alert.md and use the Courier skill for SDK details.

1. Create the template in my Courier workspace by running setup.ts from that page with COURIER_API_KEY from our environment. Save the template ID it prints as COURIER_NEW_DEVICE_TEMPLATE_ID.
2. Find where this codebase signs a user in and decides whether the device is new, and send the alert there with the recipe's send code. Pass device, location, and signed_in_at (an ISO timestamp) from our own data. Replace the placeholder product name (Acme) and the Secure your account link (https://app.acme.com/security) in the template with ours.
3. Match this codebase's language, file layout, and error handling. Never hardcode the API key.
4. When you're done, send one alert to a test user and show me both deliveries, email and Inbox.
```

## Data fields

| Field | Type | Example | Note |
| --- | --- | --- | --- |
| device | string | Chrome on macOS |  |
| location | string | Austin, Texas, US | An approximate place is enough |
| signed_in_at | string | 2026-10-15T14:42:00Z | An ISO timestamp. The template formats it as a date and time in the recipient's timezone |

## FAQ

### How do I send a new device login alert?

Detect the new device in your app, then send this template with the device, location, and time. Routing method all sends the email and the Inbox copy at the same time.

### What should a new device login alert include?

The device, an approximate location, the time, and a clear way to act if it wasn't them. Keep it short, so people can decide in a few seconds.

### What happens if the user has no email address?

With routing method all, Courier sends on every channel it can. With no email address on the profile, the Inbox copy still goes out and the email is skipped.

### How do I decide what counts as a new device?

That's your app's call: Courier delivers the alert but doesn't detect devices. A common approach is to remember a device after a successful sign-in and send the alert when a sign-in comes from one you haven't seen.

## Related docs

- [Routing and fallback](https://www.courier.com/docs/send/routing)
- [Send to the Inbox](https://www.courier.com/docs/in-app/send-to-the-inbox)
- [Send a message](https://www.courier.com/docs/send/overview)

## Related recipes

- [Password reset](https://www.courier.com/recipes/password-reset-email): Send a password reset email with a link that expires, and never deliver a stale one.
- [One-time passcode](https://www.courier.com/recipes/otp-sms-email-fallback): Send a login code by SMS, fall back to email, and stop retrying once the code is stale.
- [Magic link email](https://www.courier.com/recipes/magic-link-email): Sign people in with an emailed link, and never deliver one that has already expired.
