> 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/partner-api.md).

# Partner API

These are the endpoints an app calls once it holds a key from the [device flow](/merithic-docs/api-reference/device-flow.md). Every request needs:

```
Authorization: Bearer updatr_9f2a…
```

Base URL: `https://updatr.merithic.com/api/updatr/v1`

You never send a studio id or a user id. The key carries who it acts as, and every query is scoped to that server-side. This is the whole security model: a valid key for studio A cannot reach studio B by asking nicely.

|                    |                                                     |
| ------------------ | --------------------------------------------------- |
| Rate limit         | 120 requests/min per key                            |
| Missing scope      | `403 insufficient_scope`                            |
| Bad or revoked key | `401 invalid_token`                                 |
| Not yours          | `404 not_found` (never 403, so ids can't be probed) |

***

## Products

Needs a **studio** key.

### List products

```
GET /v1/products
```

Scope: `products:read`

```json
{
  "products": [
    {
      "id": "8f3c…",
      "slug": "combat-system",
      "title": "Combat System",
      "tagline": "Melee and ranged, ready to drop in",
      "price_robux": 500,
      "discoverable": true,
      "licensing_enabled": true,
      "current_version": "1.4.2"
    }
  ]
}
```

### Create a product

```
POST /v1/products
```

Scope: `products:write`

```json
{ "title": "Combat System", "tagline": "…", "description": "…", "price_robux": 500 }
```

Only `title` is required. The slug is generated from the title and de-duplicated.

```json
{ "ok": true, "id": "8f3c…", "slug": "combat-system", "discoverable": false }
```

{% hint style="info" %}
A product created through the API is always a **draft**. It has no file yet, and a product with nothing to deliver must never be listed. Upload a version in the cockpit, then flip it discoverable.
{% endhint %}

### Change a product

Same endpoint, with an `id`:

```json
{ "id": "8f3c…", "price_robux": 650, "discoverable": true }
```

Send only the fields you're changing. Anything you omit is left alone.

| Field          | Notes                              |
| -------------- | ---------------------------------- |
| `title`        | Up to 80 characters                |
| `tagline`      | Up to 140                          |
| `description`  | Up to 5000                         |
| `price_robux`  | 0 to 1,000,000, whole numbers      |
| `discoverable` | Whether it shows on the storefront |

Editing a product that isn't the key's studio returns `404`.

***

## Licenses

Needs a **studio** key. A license is what says a person owns a product.

### See who owns a product

```
GET /v1/licenses?product_id=8f3c…
```

Scope: `licenses:read`

```json
{
  "licenses": [
    { "id": "…", "roblox_id": "1234567", "verified_at": "2026-01-04T…", "is_active": true, "source": "gamepass" }
  ]
}
```

{% hint style="warning" %}
The runtime license **key** is never returned, by any endpoint, at any scope. It is the buyer's secret; an app that could read it could impersonate every buyer at once. If you're building a key checker, use [`/check`](/merithic-docs/api-reference/endpoints/check.md).
{% endhint %}

### Grant a license

```
POST /v1/licenses
```

Scope: `licenses:write`

```json
{ "product_id": "8f3c…", "user": "Builderman" }
```

`user` takes a Roblox username or a numeric id. A runtime key is minted, and any Discord product roles the grant earns are handed out.

Returns `409 already_owned` if they already have it.

### Revoke a license

```
DELETE /v1/licenses?product_id=8f3c…&user=Builderman
```

Scope: `licenses:write`

Deactivates it and clears the runtime key. The row stays, deliberately: deleting it would let a re-verified Gamepass silently restore access you just took away.

***

## Library

Needs a **library** key. Everything here is scoped to the person who approved it.

### What they own

```
GET /v1/library
```

Scope: `library:read`

```json
{
  "products": [
    {
      "id": "8f3c…",
      "slug": "combat-system",
      "title": "Combat System",
      "studio": "Lithora",
      "current_version": "1.4.2",
      "acquired_at": "2026-01-04T…"
    }
  ]
}
```

Paused licenses are excluded, so this matches what they can actually download.

### Versions of one product

```
GET /v1/library?product_id=8f3c…&versions=1
```

Scope: `library:read`

```json
{
  "versions": [
    { "id": "a1…", "version": "1.4.2", "size_bytes": 184320, "sha256": "…", "live": true,  "created_at": "…" },
    { "id": "b2…", "version": "1.4.1", "size_bytes": 183104, "sha256": "…", "live": false, "created_at": "…" }
  ]
}
```

`live: true` is the current release. The rest are older builds, which is the point: a buyer can roll back after a bad update without waiting on the seller.

### Download

```
GET /v1/library/download?product_id=8f3c…
GET /v1/library/download?product_id=8f3c…&version_id=b2…
```

Scope: `library:download`

Omit `version_id` for the live build; pass an `id` from the versions list to get an older one. Responds with the file itself, plus `X-Updatr-Version`.

The build is prepared per buyer on the way out: their license key is injected, then the seller's Obfuscator+ rules are applied. An app never receives an unprotected copy, and one buyer's download is not another's.

***

## Errors

Shape is the same everywhere:

```json
{ "error": "insufficient_scope", "error_description": "This key is missing the \"licenses:write\" scope." }
```

| Code                 | Status | Meaning                                                            |
| -------------------- | ------ | ------------------------------------------------------------------ |
| `invalid_request`    | 400    | A required field is missing or malformed.                          |
| `invalid_token`      | 401    | Key unknown, revoked, or expired.                                  |
| `insufficient_scope` | 403    | Valid key, wrong scope — or a studio call made with a library key. |
| `not_found`          | 404    | It doesn't exist, or it isn't yours.                               |
| `already_owned`      | 409    | That user already holds this license.                              |
| `server_error`       | 500    | Ours. Retry.                                                       |

Rate limiting answers `429` with a `Retry-After` header in seconds. Branch on the status code there, not on the body.

Handle `401` by running the device flow again: it means the user revoked you, or the key aged out after 60 days unused.


---

# 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/partner-api.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.
