# Sign a person in for an AI app

**POST** `/api/embed/v1/oauth-requests/{requestId}/complete`

Says who the person is, once they signed in to your product and pressed Continue.

Needs a partner key: `Authorization: Bearer gre_…`, from [Platform › Partner keys](https://docs.gorunner.io/embed/platform-screens#partner-keys). Call it from your server only.

Embed · Self-hosted with an Embed license

## Parameters

### Path

- `requestId` · Required · string: The request’s ID: the `gr_request` parameter on the address the person arrived at.

### Body

- `user` · Required · object: The person: your `id`, `name` and, if you like, `email`.
- `account` · Required · object: The account: your `id` and `name`. It must be the request’s.
- `workspace` · Optional · object or null: A workspace by your `id`. Left out: the default workspace.
- `role` · Optional · string: Without `provision`: if sent, it must be their role there.
- `provision` · Optional · boolean: `false` (the default): they must already be a member. `true`: made as a login code makes them. A membership it made stays even if the browser never arrives.

**What are the rarely used parameters?**

- `turnOff` · Optional · array of strings: Switches to turn off for this connection.

## After it

The installation shows its own consent page, where the person chooses what the app may do. Once per request; a retry after a success answers `OAUTH_REQUEST_DONE`.

The connection is checked on every request: it pauses while a switch it needs is off, and ends when the person leaves, drops below this role or is deactivated, when the account is deleted, or when **this key** is revoked. See [Let customers sign in AI apps](https://docs.gorunner.io/embed/ai-app-sign-in).

## Responses

- 201 Created

  Send the browser to `continueUrl` with a 303. It works once, for 60 seconds; never log it.

- 400 `VALIDATION_ERROR`

  A field isn’t valid, as for a login code. `details.field` names it.

- 403 `WORKSPACE_LIMIT`

  With `provision: true`, a new workspace would pass the license’s workspace limit.

- 403 `FEATURE_OFF`

  A switch AI app sign-in needs is off, or is in `turnOff`. `details.switch`: `tokens`, `mcp`, `mcpSignIn`, or `mcpBuild` for a build address. It carries `details.continueUrl`: send the browser there so the AI app is told.

- 404 `OAUTH_REQUEST_NOT_FOUND`

  No such request, or it expired. No `continueUrl`.

- 409 `OAUTH_REQUEST_ACCOUNT_MISMATCH`

  `account.id` isn’t the request’s (`details.expected`). The request isn’t used up.

- 409 `OAUTH_REQUEST_DONE`

  It was already answered.

- 409 `NOT_A_MEMBER`

  Without `provision`, the person isn’t a member of the workspace. It carries `details.continueUrl`: send the browser there so the AI app is told.

- 409 `ROLE_DIFFERS`

  Without `provision`, `role` isn’t their role there. It carries `details.continueUrl`: send the browser there so the AI app is told.

- 409 `ACCOUNT_DELETED`

  The account is deleted. It carries `details.continueUrl`: send the browser there so the AI app is told.

- 500 `INTERNAL_ERROR`

  Something failed on the installation’s side. Try again.

Any call can also answer: [`401 UNAUTHORIZED`](https://docs.gorunner.io/errors#UNAUTHORIZED) · [`403 EMBED_NOT_LICENSED`](https://docs.gorunner.io/errors#EMBED_NOT_LICENSED) · [`403 IP_NOT_ALLOWED`](https://docs.gorunner.io/errors#IP_NOT_ALLOWED) · [`413 PAYLOAD_TOO_LARGE`](https://docs.gorunner.io/errors#PAYLOAD_TOO_LARGE) · [`415 INVALID_CONTENT_TYPE`](https://docs.gorunner.io/errors#INVALID_CONTENT_TYPE) · [`429 RATE_LIMITED`](https://docs.gorunner.io/errors#RATE_LIMITED) · [`503 BUSY`](https://docs.gorunner.io/errors#BUSY). See [Errors](https://docs.gorunner.io/embed/api#errors).

Request

**curl**

```bash
GORUNNER="https://automations.example.com"
GORUNNER_EMBED_KEY="YOUR_PARTNER_KEY"  # gre_…
REQUEST_ID="q7Kd2mXr9TfL0aZc3vBn8wYe"  # gr_request from the address

URL="$GORUNNER/api/embed/v1/oauth-requests/"
URL="${URL}$REQUEST_ID/complete"
curl -X POST "$URL" \
  -H "Authorization: Bearer $GORUNNER_EMBED_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "user": {
      "id": "ada-123",
      "name": "Ada Lovelace"
    },
    "account": {
      "id": "acct-bakery",
      "name": "Bakery Ltd"
    }
  }'
```

**JavaScript**

```js
const GORUNNER = "https://automations.example.com";
const KEY = process.env.GORUNNER_EMBED_KEY; // gre_…
const REQUEST_ID = encodeURIComponent("q7Kd2mXr9TfL0aZc3vBn8wYe");

const res = await fetch(
  `${GORUNNER}/api/embed/v1/oauth-requests/` +
    `${REQUEST_ID}/complete`,
  {
    method: "POST",
    headers: {
        Authorization: `Bearer ${KEY}`,
        "Content-Type": "application/json",
      },
    body: JSON.stringify({
        user: { id: "ada-123", name: "Ada Lovelace" },
        account: {
          id: "acct-bakery",
          name: "Bakery Ltd",
        },
      }),
  },
);
const { data } = await res.json();
```

**Python**

```python
import os, requests
from urllib.parse import quote

GORUNNER = "https://automations.example.com"
KEY = os.environ["GORUNNER_EMBED_KEY"]  # gre_…
REQUEST_ID = quote("q7Kd2mXr9TfL0aZc3vBn8wYe", safe="")

res = requests.post(
    f"{GORUNNER}/api/embed/v1/oauth-requests/"
    f"{REQUEST_ID}/complete",
    headers={"Authorization": f"Bearer {KEY}"},
    json={
      "user": {
        "id": "ada-123",
        "name": "Ada Lovelace",
      },
      "account": {
        "id": "acct-bakery",
        "name": "Bakery Ltd",
      },
    },
)
data = res.json()["data"]
```

Response · 201 Created, shortened

```json
{
  "data": {
    "continueUrl": "https://automations.example.com/api/embed/oauth/return?request=q7Kd2mXr9TfL0aZc3vBn8wYe&ticket=…",
    "expiresAt": "2026-10-25T09:01:00Z",
    "expiresIn": 60,
    "account": {
      "id": "acct-bakery",
      "status": "active",
      "created": false
    },
    "workspace": {
      "id": null,
      "default": true,
      "created": false
    },
    "user": {
      "id": "ada-123",
      "created": false
    },
    "role": "editor"
  }
}
```

**Every field**

### Body

- `user` · Required · object
  - `id` · Required · string
  - `name` · Required · string
  - `email` · Optional · string or null
- `account` · Required · object
  - `id` · Required · string
  - `name` · Required · string
- `workspace` · Optional · object or null
  - `id` · Optional · string
  - `name` · Optional · string or null
- `role` · Optional · string: Without provisioning, optional, and when given it must be the person’s role there. With `provision: true`, required. One of `viewer`, `operator`, `editor`.
- `turnOff` · Optional · array of strings
- `provision` · Optional · boolean: false: the person must already be a member of the workspace. true: made as a login code makes them. Default `false`.

### Response · 201 Created

- `data` · object
  - `continueUrl` · URL
  - `expiresAt` · time
  - `expiresIn` · integer
  - `account` · object
    - `id` · string
    - `status` · string: One of `active`, `suspended`.
    - `created` · boolean
  - `workspace` · object
    - `id` · string or null
    - `default` · boolean
    - `created` · boolean
  - `user` · object
    - `id` · string
    - `created` · boolean
  - `role` · string: One of `viewer`, `operator`, `editor`.

Related: [Read a sign-in request](https://docs.gorunner.io/embed/api/ai-apps/get-oauth-request) · [Turn a sign-in request down](https://docs.gorunner.io/embed/api/ai-apps/deny-oauth-request)

Source: https://docs.gorunner.io/embed/api/ai-apps/complete-oauth-request
