> For the complete documentation index, see [llms.txt](https://merithic.gitbook.io/merithic-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://merithic.gitbook.io/merithic-docs/api-reference/device-flow.md).

# Connecting an app (device flow)

A Roblox Studio plugin can't open a browser redirect back to itself, so it can't use a normal OAuth login. Updatr implements the **Device Authorization Grant** ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)) instead:

```
1. App  →  Updatr    "start an authorization"      → gets a short code
2. App  →  User      "open this page, enter WXYZ-1234"
3. User →  Updatr    approves, picks which studio
4. App  →  Updatr    polls until approved          → receives an API key
```

The user never types a password into the plugin, and the plugin never sees their account — only a key scoped to one studio.

{% hint style="info" %}
This is for tools acting on someone's behalf: managing a **studio**, or reading a **buyer's** library. It is NOT how a product verifies a buyer at runtime — that's [`/check`](/merithic-docs/api-reference/endpoints/check.md) and [`/unlock`](/merithic-docs/api-reference/endpoints/unlock.md).
{% endhint %}

## Two kinds of key

The scopes you ask for decide which kind of key you get. Pick one; you can't mix them in a single key.

|                 | Studio key                               | Library key                |
| --------------- | ---------------------------------------- | -------------------------- |
| Acts as         | A studio                                 | The person                 |
| Who can approve | Owner or collaborator                    | Anyone with an account     |
| Scopes          | `packages:*`, `products:*`, `licenses:*` | `library:*`                |
| Consent screen  | Shows the studio being authorized        | No studio to pick          |
| Reaches         | That studio's products and licenses      | Only what that person owns |

A package manager that publishes for a studio wants a studio key. A tool that installs what the user has bought wants a library key. If you need both, run the flow twice and keep two keys.

## 1. Start the authorization

```
POST https://updatr.merithic.com/api/updatr/oauth/device/code
```

```json
{ "client_id": "updatr-studio-plugin", "scope": "packages:read packages:write" }
```

`scope` is **required** — a space-separated list of what the app actually needs. You can ask for less than your client is registered for, never more. Asking for everything by default is how integrations end up over-privileged, so the field is deliberately not optional.

```json
{
  "device_code": "hR8x…",
  "user_code": "WXYZ-1234",
  "verification_uri": "https://updatr.merithic.com/cockpit/authorize",
  "verification_uri_complete": "https://updatr.merithic.com/cockpit/authorize?code=WXYZ-1234",
  "expires_in": 600,
  "interval": 5
}
```

{% hint style="danger" %}
`device_code` is a secret. Show the user `user_code`; never display or log the device code.
{% endhint %}

## 2. Send the user to approve

Show `user_code` and open `verification_uri_complete` (which prefills it). They sign in if needed, pick which studio to authorize, and see exactly what's being requested before approving.

The code expires in 10 minutes.

## 3. Poll for the key

```
POST https://updatr.merithic.com/api/updatr/oauth/device/token
```

```json
{ "device_code": "hR8x…" }
```

Poll every `interval` seconds. While waiting you get HTTP 400 with:

| `error`                 | What to do                                          |
| ----------------------- | --------------------------------------------------- |
| `authorization_pending` | Not approved yet. Keep polling.                     |
| `slow_down`             | You're polling too fast. Back off, then continue.   |
| `access_denied`         | The user said no. Stop.                             |
| `expired_token`         | Too slow, or already claimed. Start over at step 1. |

On approval:

```json
{
  "access_token": "updatr_9f2a…",
  "token_type": "Bearer",
  "scope": "packages:read packages:write",
  "studio_id": "1234567"
}
```

`studio_id` is `null` on a library key, since there's no studio behind it.

{% hint style="warning" %}
**This is the only time the key exists.** Updatr stores only a hash of it — it can't be shown again or recovered. Save it immediately; if you lose it, run the flow again.
{% endhint %}

## 4. Use the key

```
Authorization: Bearer updatr_9f2a…
```

The key is bound to one studio and one scope set. You never send a studio id — the key already carries it.

## Scopes

**Studio scopes** — need a studio, and the approver needs the matching permission in it:

| Scope            | Grants                                 | Studio permission needed |
| ---------------- | -------------------------------------- | ------------------------ |
| `packages:read`  | See the studio's packages and versions | `manage_products`        |
| `packages:write` | Publish and update packages            | `manage_products`        |
| `products:read`  | See products                           | `manage_products`        |
| `products:write` | Create and edit products               | `manage_products`        |
| `licenses:read`  | See who owns licenses                  | `manage_licenses`        |
| `licenses:write` | Grant and revoke licenses              | `manage_licenses`        |

**Library scopes** — act on the approver's own purchases, no studio involved:

| Scope              | Grants                                          |
| ------------------ | ----------------------------------------------- |
| `library:read`     | List what they own, and each product's versions |
| `library:download` | Download a product they own, any version        |

A collaborator can only grant a scope they hold themselves. Someone without `manage_licenses` in that studio can't approve `licenses:write`, no matter what the app asks for — the approve step filters the list server-side, then issues a key carrying only what survived. Check the `scope` in the token response: it is what you actually got, which may be less than you asked for.

If nothing survives the filter, approval fails with 403 rather than handing back a useless key.

See [Partner API](/merithic-docs/api-reference/partner-api.md) for what each scope actually lets you call.

## Logging out

A client revokes its own key:

```
POST https://updatr.merithic.com/api/updatr/oauth/revoke
Authorization: Bearer updatr_9f2a…
```

Do this on "log out" rather than only clearing local storage — otherwise a copied key keeps working after the user thinks they've signed out.

## Managing connections

**Studio → Connected apps** lists everything authorized, what it can do, and when it was last used. Revoking takes effect on the next request.

Keys unused for 60 days are revoked automatically.

## Don't let users paste keys

Never add a "paste your API key here" command to a client. There's no legitimate case for it once the device flow exists, and it's a ready-made phishing route — "run this to fix your install" is all it takes to plant an attacker's key, or to get a user to leak their own somewhere it gets logged.

Keys should only ever arrive through the poll in step 3.

## Rate limits

|                         | Limit                                                 |
| ----------------------- | ----------------------------------------------------- |
| `POST /device/code`     | 20/min per IP                                         |
| Polling `/device/token` | Enforced per device code via `interval` + `slow_down` |
| Code entry attempts     | 5 wrong guesses burns that session                    |
| API requests with a key | 120/min per key                                       |

## Errors

Errors follow OAuth 2.0 shape: HTTP 400 with `{ "error": "...", "error_description": "..." }`. A client written against any other device-flow provider will already handle these.

| Code                 | Meaning                                                    |
| -------------------- | ---------------------------------------------------------- |
| `invalid_request`    | A required field is missing.                               |
| `invalid_client`     | Unknown or disabled `client_id`.                           |
| `invalid_scope`      | None of the requested scopes are available to this client. |
| `invalid_token`      | The API key is unknown, revoked, or expired. (401)         |
| `insufficient_scope` | Valid key, but it lacks the scope for this call. (403)     |

## Registering a client

Clients are registered by Updatr. Get in touch with the app name, a one-line description shown on the consent screen, and the scopes you need.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://merithic.gitbook.io/merithic-docs/api-reference/device-flow.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
