---
title: "Link to a screen in your app"
description: "Declare your screens once, then send links that open the right screen in the app or on the web, invite people, and revoke a link."
section: "guides"
status: "Building for 0.4"
spec: "links"
canonical: "https://mainsdk.dev/docs/guides/link-to-a-screen"
updated: "2026-10-28"
---

# Link to a screen in your app

> Declare your screens once, then send links that open the right screen in the app or on the web, invite people, and revoke a link.

Status: Building for 0.4 · Canonical: https://mainsdk.dev/docs/guides/link-to-a-screen · Updated: 2026-10-28

You will declare one screen, make a link to it, resolve it and revoke it. It takes about 15 minutes. Every link is a getqr.id share, so it expires and shows nothing of the screen or its params.

## You need

- A MainSDK app with `share` working (see [share anything](https://mainsdk.dev/docs/guides/share-anything)).

## 1. Declare the screens

```yaml title="main.yaml"
links:
  base: https://app.example.com
  routes:
    order: { web: /orders/:id, app: orders/:id, params: [id] }
```

## 2. Make a link

```ts title="app/orders/actions.ts"
import { links } from "mainsdk";

export async function orderLink(orderId: string) {
  const link = await links.create({ screen: "order", params: { id: orderId } }, { expiresIn: "14d" });
  return link.url;
}
```

A screen you did not declare, or different params, fail with `MAIN-LINK-0002`.

## 3. Resolve an incoming link

```ts title="app/open/route.ts"
import { links } from "mainsdk";

export async function GET(req: Request) {
  const target = await links.resolve(new URL(req.url).searchParams.get("u") ?? "");
  return Response.json(target);   // { kind, route, valid, reason? }
}
```

`resolve` never throws for an unknown URL. `valid` is false with `reason` `expired`, `revoked`, `used-up` or `unknown`.

## 4. Invite someone

```ts title="app/lists/[id]/invite/route.ts"
import { links } from "mainsdk";

export async function inviteJane(id: string) {
  return links.invite(
    { screen: "list", params: { id } },
    { email: "jane@example.com", uses: 1, expiresIn: "7d" },
  );
}
```

The person accepts with `links.accept(req, token)` after signing in.

## 5. Revoke

```ts title="app/orders/revoke.ts"
import { links } from "mainsdk";

export async function revoke(id: string) {
  await links.revoke(id);
}
```

## Check it worked

```ts title="app/orders/links.test.ts"
import { it } from "node:test";
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";
import { links } from "mainsdk";

it("links AC3 reports a revoked link", async () => {
  createTestApp({ main: "./main.yaml" });
  const link = await links.create({ screen: "order", params: { id: "ord_1" } });
  await links.revoke(link.id);
  assert.partialDeepStrictEqual(await links.resolve(link.url), { valid: false, reason: "revoked" });
});
```

## Troubleshooting

- `MAIN-LINK-0001` (410): the invite is used up, expired, revoked or for another email.
- `MAIN-LINK-0003`: a route param identifies a person, such as `email`. Pass a record id instead.
- `MAIN-LINK-0004`: `links.apps` is missing what the association files need, or `links.base` is not HTTPS.

See the [links interface](https://mainsdk.dev/docs/interfaces/links).
