# Redirect login links

Source: https://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 and partner-managed users 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.

> **Managed identity account**
>
> The supplied `external_user_id` resolves an embedded identity in the API key’s workspace. On first use, Signal provisions a dedicated partner-managed hosted profile for that workspace and external identifier. It has no password and never matches, links, or signs into an existing Ceyo account by email.

> **Access and destination checks**
>
> The API key needs `login_links:manage`, and the requested project or location package must enable Customer access through `pricing.frontend_delivery_enabled`. This single entitlement enables both hosted Signal UI and redirect login links. Signal verifies the package entitlement and embedded access 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 through its dedicated partner-managed account. 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. |
| `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. 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**
>
> `redirect_path` controls the first page opened inside Signal. `return_url` controls where the user may return afterward. Signal does not accept arbitrary origins, JavaScript URLs, protocol-relative URLs, or URLs containing credentials. After sign-in, hosted Signal displays a Return to partner action only when a validated `return_url` was supplied.

#### Response envelope

`login_link`:**LoginLink**

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/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 \| customer\_access\_not\_enabled |  | The API key lacks login\_links:manage, the package does not enable Customer access, or the embedded identity lacks access to the requested scope. |
| `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

`login_link`:**LoginLink**

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

`login_link`:**LoginLink**

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. |
