# Redirect login links

Source: https://preview.ceyo.ai/docs/signal/redirect-login-links

### Redirect login links

Send an embedded identity into hosted Signal with a short-lived, one-time URL.

> **Hosted roles**
>
> The identity’s grant controls authorization after sign-in. `viewer` is read-only, `editor` manages supported operational features, and `admin` additionally manages settings within the granted project or location. A login link authenticates the identity and never elevates its role.

> **Server-side only**
>
> Create, inspect, and revoke login links from your backend with a Signal API key. Never expose an API key in browser code, logs, or a public URL. Treat each returned login URL as a temporary credential.

> **Direct embedded authentication**
>
> The supplied `external_user_id` resolves an embedded identity in the API key’s workspace. Redemption creates a hosted Platform session directly for that identity. Signal does not create a normal Ceyo user, mirror its grants, or match an existing account by email.

> **Access and destination checks**
>
> The API key needs `login_links:manage`, and the requested project or location package must be active. The workspace must also have hosted white-label enabled. Signal verifies the API key, package, identity, and grant both when the link is created and when it is used. Return URLs must use an exact HTTPS origin in the API key’s `allowed_origins`; redirect paths stay within hosted Signal.

### Create redirect login link

`POST /login-links`

Creates a one-time hosted Signal login URL for an embedded identity and a project or location it can access.

#### Request body

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `external_user_id` | string |  | Required external identifier of the embedded identity that will sign in directly. Maximum: 200 characters. |
| `project_id` | project UUID \| project external ID |  | Required project identifier. The embedded identity must already have access to this project. |
| `location_id` | location UUID \| location external ID \| null |  | Optional location within the project. When supplied, the embedded identity must have access to this location. |
| `landing` | auto \| projects \| locations \| null |  | Optional simple landing choice. auto applies resource-count routing, projects opens the Projects list, and locations opens the selected project’s Locations list. Cannot be combined with location\_id or redirect\_path. |
| `redirect_path` | string \| null |  | Optional relative path to open in hosted Signal after sign-in. Must begin with one slash and cannot contain a scheme, host, backslash, or protocol-relative URL. Defaults to the selected project or location home. |
| `return_url` | https URL \| null |  | Optional URL shown as the safe return destination from Signal. It overrides the workspace default return URL. Its origin must be registered for the API key; fragments and embedded credentials are rejected. |
| `expires_in` | integer |  | Optional lifetime in seconds. Defaults to 900 (15 minutes); minimum: 60; maximum: 3,600 (1 hour). |

> **Safe navigation**
>
> Omit both destination fields to open the selected project or location overview. Use `landing: "auto"`, `landing: "projects"`, or `landing: "locations"` for common destinations. Use `redirect_path` for an advanced internal page; it cannot be combined with `landing`. `return_url` controls where the user may return afterward and overrides the workspace default. Signal does not accept arbitrary origins, JavaScript URLs, protocol-relative URLs, path traversal, or URLs containing credentials. After sign-in, hosted Signal displays the workspace-configured return action when either a validated `return_url` or a workspace default return URL is available.

#### Response envelope

The requested or resulting login link.

#### LoginLink

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Stable login link identifier used for lifecycle requests. |
| `url` | string \| null |  | Hosted Signal sign-in URL. Returned only when the link is created; lifecycle responses return null. |
| `status` | pending \| used \| expired \| revoked |  | Current one-time login link status. |
| `expires_at` | datetime |  | Time the pending link expires, in ISO 8601 format. |
| `used_at` | datetime \| null |  | Time the link was successfully used, or null if it was not used. |
| `created_at` | datetime |  | Time the link was created, in ISO 8601 format. |

#### Request and response

```curl
curl --request POST \
  --url 'https://api.signal.ceyo.ai/v1/login-links' \
  --header 'Authorization: Bearer ceyo_platform_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "external_user_id": "customer-user-4821",
  "project_id": "partner-project-acme",
  "location_id": "a1308d14-149c-4dd7-a4c5-295ac9090f58",
  "redirect_path": "/workspaces/{workspace_id}/projects/e6c96c98-d777-40e0-94ec-48931f57782f/locations/a1308d14-149c-4dd7-a4c5-295ac9090f58/visibility",
  "return_url": "https://portal.partner.example/customers/4821",
  "expires_in": 900
}'
```

```json
HTTP/1.1 201 Created

{
  "login_link": {
    "id": "7a73ed11-d5dc-4b6a-8681-d9e571d2a991",
    "url": "https://signal.ceyo.ai/login-links/7a73ed11-d5dc-4b6a-8681-d9e571d2a991?token=ceyo_login_...",
    "status": "pending",
    "expires_at": "2026-07-31T10:15:00Z",
    "used_at": null,
    "created_at": "2026-07-31T10:00:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The JSON body was not provided, is malformed, or contains an unknown field. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden \| white\_label\_not\_enabled \| package\_not\_active |  | The API key lacks login\_links:manage, the embedded identity lacks an active grant to the requested scope, hosted white-label is disabled for the workspace, or the assigned package is not active. |
| `404` | not\_found |  | The embedded identity, project, or location was not found for this API key. |
| `422` | validation\_failed |  | An identifier, expiry, redirect path, or return URL does not meet the documented constraints. |
| `429` | rate\_limit\_exceeded |  | Too many login links were created or too many requests were made. |

### Get login link

`GET /login-links/{login_link_id}`

Returns the current state of a login link without changing it.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `login_link_id` | uuid | Required | Login link identifier returned by the create endpoint. |

#### Response envelope

The requested or resulting login link.

#### LoginLink

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Stable login link identifier used for lifecycle requests. |
| `url` | string \| null |  | Hosted Signal sign-in URL. Returned only when the link is created; lifecycle responses return null. |
| `status` | pending \| used \| expired \| revoked |  | Current one-time login link status. |
| `expires_at` | datetime |  | Time the pending link expires, in ISO 8601 format. |
| `used_at` | datetime \| null |  | Time the link was successfully used, or null if it was not used. |
| `created_at` | datetime |  | Time the link was created, in ISO 8601 format. |

> **Read-only status**
>
> This request returns `pending`, `used`, `expired`, or `revoked` and never changes the link. Used, expired, and revoked are terminal states. A terminal link cannot be used, revoked, or reactivated.

#### Request and response

```curl
curl --request GET \
  --url 'https://api.signal.ceyo.ai/v1/login-links/7a73ed11-d5dc-4b6a-8681-d9e571d2a991' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "login_link": {
    "id": "7a73ed11-d5dc-4b6a-8681-d9e571d2a991",
    "url": null,
    "status": "pending",
    "expires_at": "2026-07-31T10:15:00Z",
    "used_at": null,
    "created_at": "2026-07-31T10:00:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The login\_link\_id is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage login links. |
| `404` | not\_found |  | The login link was not found for this API key. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |

### Revoke login link

`DELETE /login-links/{login_link_id}`

Revokes a pending login link and returns its terminal revoked state.

#### Path parameters

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `login_link_id` | uuid | Required | Login link identifier returned by the create endpoint. |

#### Response envelope

The requested or resulting login link.

#### LoginLink

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `id` | uuid |  | Stable login link identifier used for lifecycle requests. |
| `url` | string \| null |  | Hosted Signal sign-in URL. Returned only when the link is created; lifecycle responses return null. |
| `status` | pending \| used \| expired \| revoked |  | Current one-time login link status. |
| `expires_at` | datetime |  | Time the pending link expires, in ISO 8601 format. |
| `used_at` | datetime \| null |  | Time the link was successfully used, or null if it was not used. |
| `created_at` | datetime |  | Time the link was created, in ISO 8601 format. |

> **Terminal-state behavior**
>
> A pending link changes to `revoked` and returns `200 OK`. Used, expired, and revoked links remain unchanged and return `409 login_link_not_pending`. Revoked links cannot be used or reactivated.

#### Request and response

```curl
curl --request DELETE \
  --url 'https://api.signal.ceyo.ai/v1/login-links/7a73ed11-d5dc-4b6a-8681-d9e571d2a991' \
  --header 'Authorization: Bearer ceyo_platform_...'
```

```json
{
  "login_link": {
    "id": "7a73ed11-d5dc-4b6a-8681-d9e571d2a991",
    "url": null,
    "status": "revoked",
    "expires_at": "2026-07-31T10:15:00Z",
    "used_at": null,
    "created_at": "2026-07-31T10:00:00Z"
  }
}
```

#### Errors

#### Error response envelope

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `error` | Error |  | Structured error payload. |

#### Error

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `code` | string |  | Stable snake\_case code suitable for programmatic handling. |
| `message` | string |  | Human-readable explanation of the failure. |
| `details` | object \| array \| null |  | Structured validation or request context when available. |
| `request_id` | string |  | Identifier to provide when requesting support. |

```json
{
  "error": {
    "code": "invalid_api_key",
    "message": "The Bearer API key is invalid.",
    "details": null,
    "request_id": "req_01K1F8M7QX4R2V9N6Y3Z0A5BCT"
  }
}
```

#### Status codes

| Name | Type | Details | Description |
| --- | --- | --- | --- |
| `400` | invalid\_request |  | The login\_link\_id is malformed. |
| `401` | invalid\_api\_key |  | The Bearer API key is absent or invalid. |
| `403` | forbidden |  | The API key cannot manage login links. |
| `404` | not\_found |  | The login link was not found for this API key. |
| `409` | login\_link\_not\_pending |  | The link is used, expired, or revoked and remains in that terminal state. |
| `429` | rate\_limit\_exceeded |  | Too many requests were made. |
