> ## Documentation Index
> Fetch the complete documentation index at: https://www.1password.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Approve in your Android app

> Present an access request to 1Password from your Android app in a bottom sheet, and get the person's grant back without leaving your app.

Partner preview

Your Android app can present an access request to 1Password without sending the person to another app. You pass 1Password the access request reference you got when you created the request. 1Password shows the person a sheet over your app that names your app and what it asked for, and they decide whether to grant it. Your app then gets back either the access grant or an error.

Your app talks to 1Password through an activity that 1Password exports for this purpose. You start it for a result with an explicit `Intent`, and Android returns the person's decision to your app.

> [!NOTE]
>
> The authorization activity is available in 1Password for Android 8.12.42 or later. See [Supported builds](/agentic-autofill/partners/supported-builds.md).

## How it works

1. Your app puts the access request reference and the protocol version in an `Intent` for the `com.onepassword.android.workloadaccess.action.REQUEST_AUTHORIZATION` action, addressed to the 1Password package.
2. Your app starts that `Intent` for a result, and 1Password opens a bottom sheet over your app.
3. 1Password shows the access request, unlocking first if needed, and the person grants or denies it. If 1Password can't show or answer the request, the sheet tells the person why.
4. Your app's activity result callback receives either an access request status, which decodes to an `OnePasswordAccessGrant`, or an error.

The grant comes back to your app directly, so you don't have to poll for the decision.

If your app can't start an activity for a result, for example because it runs in a browser, [open the access request link instead](#open-an-access-request-link-instead).

## Get the access request reference

Create the access request first. The reference is the value 1Password reads to find the account and the request, including what you asked for and why. It's the `access_request_reference` value in the `appLink` returned when you create the request, by the browser extension's `createAccessRequest` or by the SDK's [`accessRequests.create`](/agentic-autofill/partners/sdk#create-an-access-request). If you create requests with the SDK, contact 1Password first.

Pass the reference unchanged. Don't decode it, edit it, or build it from parts. Like the approval link, it carries your request text, so keep it out of logs and model context.

## Add the integration types

1Password provides these Kotlin definitions. They use [`kotlinx.serialization`](https://github.com/Kotlin/kotlinx.serialization) to decode the grant. Add them to your app module:

```kotlin theme={null}
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/** Starts 1Password's authorization sheet and decodes the person's decision. */
class OnePasswordAccessRequestContract :
    ActivityResultContract<OnePasswordAccessRequest, OnePasswordAccessRequestResult>() {

    override fun createIntent(context: Context, input: OnePasswordAccessRequest): Intent =
        Intent(ACTION_REQUEST_AUTHORIZATION)
            .setPackage(ONEPASSWORD_PACKAGE)
            .putExtra(EXTRA_PROTOCOL_VERSION, input.version)
            .putExtra(EXTRA_ACCESS_REQUEST_REFERENCE, input.accessRequestReference)

    override fun parseResult(resultCode: Int, intent: Intent?): OnePasswordAccessRequestResult {
        if (resultCode != Activity.RESULT_OK) return OnePasswordAccessRequestResult.Cancelled

        return when (intent?.getStringExtra(EXTRA_STATUS)) {
            STATUS_AUTHORIZED -> intent?.getStringExtra(EXTRA_ACCESS_REQUEST_STATUS)
                ?.let(OnePasswordAccessRequestResult::Granted)
                ?: OnePasswordAccessRequestResult.Failed(error = null)
            STATUS_DENIED -> OnePasswordAccessRequestResult.Denied
            // STATUS_ERROR, and any status added in a later version.
            else -> OnePasswordAccessRequestResult.Failed(
                error = OnePasswordAccessRequestError.fromCode(intent?.getStringExtra(EXTRA_ERROR)),
            )
        }
    }

    companion object {
        const val ONEPASSWORD_PACKAGE = "com.onepassword.android"
        private const val NAMESPACE = "com.onepassword.android.workloadaccess"

        const val ACTION_REQUEST_AUTHORIZATION = "$NAMESPACE.action.REQUEST_AUTHORIZATION"

        const val EXTRA_PROTOCOL_VERSION = "$NAMESPACE.extra.PROTOCOL_VERSION"
        const val EXTRA_ACCESS_REQUEST_REFERENCE = "$NAMESPACE.extra.ACCESS_REQUEST_REFERENCE"

        const val EXTRA_STATUS = "$NAMESPACE.extra.STATUS"
        const val EXTRA_ACCESS_REQUEST_STATUS = "$NAMESPACE.extra.ACCESS_REQUEST_STATUS"
        const val EXTRA_ERROR = "$NAMESPACE.extra.ERROR"

        const val STATUS_AUTHORIZED = "authorized"
        const val STATUS_DENIED = "denied"
        const val STATUS_ERROR = "error"
    }
}

/** The access request your app sends to 1Password. */
data class OnePasswordAccessRequest(
    /** The access request reference your app was given when it created the access request. */
    val accessRequestReference: String,
) {
    /** The version of this request's structure. */
    val version: Int = CURRENT_VERSION

    companion object {
        /** The protocol version this definition encodes. */
        const val CURRENT_VERSION = 1
    }
}

/** How the access request ended. */
sealed interface OnePasswordAccessRequestResult {
    /** The person granted access. Decode [accessRequestStatus] as an [OnePasswordAccessGrant]. */
    data class Granted(val accessRequestStatus: String) : OnePasswordAccessRequestResult

    /** The person denied the request. */
    data object Denied : OnePasswordAccessRequestResult

    /** 1Password couldn't process the request. [error] is `null` for a code this definition doesn't know. */
    data class Failed(val error: OnePasswordAccessRequestError?) : OnePasswordAccessRequestResult

    /** The person closed the sheet without deciding, or the system closed it. The access request is still pending. */
    data object Cancelled : OnePasswordAccessRequestResult
}

/** The access granted by 1Password: the access request's status, read after the person granted it. */
@Serializable
data class OnePasswordAccessGrant(
    val version: Int,
    val payload: Payload,
) {
    @Serializable
    data class Payload(
        val state: State,
        val resolved: List<ResolvedEntry>,
    )

    @Serializable
    enum class State {
        @SerialName("pending") Pending,
        @SerialName("resolved") Resolved,
        @SerialName("denied") Denied,
        @SerialName("failed") Failed,
        @SerialName("expired") Expired,
    }

    @Serializable
    data class ResolvedEntry(
        val entryId: String?,
        val reference: CredentialReference,
    )

    @Serializable
    data class CredentialReference(
        val reference: String,
    )
}

/** Errors returned by the 1Password authorization activity. */
enum class OnePasswordAccessRequestError(val code: String) {
    InvalidRequest("invalid_request"),
    UnsupportedVersion("unsupported_version"),
    AccountNotFound("account_not_found"),
    FeatureNotEnabled("feature_not_enabled"),
    RequestUnavailable("request_unavailable"),
    RequestFailed("request_failed"),
    InternalError("internal_error"),
    ;

    companion object {
        fun fromCode(code: String?): OnePasswordAccessRequestError? = entries.firstOrNull { it.code == code }
    }
}
```

### The request

The request travels as two `Intent` extras:

| Extra | Type | Description |
| - | - | - |
| `com.onepassword.android.workloadaccess.extra.PROTOCOL_VERSION` | `Int` | Always `1` for this version of the protocol. `OnePasswordAccessRequest` sets it for you. It covers only the structure of this request and of the grant that comes back. The access request reference is versioned separately. |
| `com.onepassword.android.workloadaccess.extra.ACCESS_REQUEST_REFERENCE` | `String` | The access request reference you got when you created the access request. |

### The access grant

The access grant is the access request's status, read from 1Password after the person granted it, together with the protocol version it's encoded in. It arrives as a JSON string in the `com.onepassword.android.workloadaccess.extra.ACCESS_REQUEST_STATUS` extra:

```json theme={null}
{
  "version": 1,
  "payload": {
    "state": "resolved",
    "resolved": [
      {
        "entryId": "<entry-id>",
        "reference": {
          "reference": "accounts/<account>/capability/login/configurations/<configuration-id>"
        }
      }
    ]
  }
}
```

| Field | Type | Description |
| - | - | - |
| `version` | `Int` | The protocol version the grant is encoded in. It matches the `PROTOCOL_VERSION` you sent. Keep it with the payload if you forward the JSON, for example to your server. |
| `payload.state` | `State` | The access request's state: `pending`, `resolved`, `denied`, `failed`, or `expired`. A grant you receive is `resolved`. The other values are the same final states the SDK's `getStatus` and the extension return, so handle them the same way, and note that `expired` isn't returned yet. See [States](/agentic-autofill/partners/approval.md#states). |
| `payload.resolved` | `List<ResolvedEntry>` | One entry per granted login. `entryId` names the request entry the login satisfies, and is `null` for a login the person added beyond the ones you asked for. Match on `entryId`, never on position. `reference` wraps the credential reference string. |

`payload` carries the `state` and `resolved` fields that the SDK's [`accessRequests.getStatus`](/agentic-autofill/partners/sdk#wait-for-the-decision) returns, except for `reference`: in the grant, each `reference` is an object that wraps the reference string, `{ "reference": "<string>" }`, while `getStatus` returns the string itself. The grant doesn't include the status `path`.

> [!NOTE]
>
> A later version of this protocol is expected to return each `reference` as a plain string, the same as `getStatus`. 1Password always answers in the version you ask for, so your app keeps working until you move to the new version.

## Declare the 1Password package

Android 11 and later hide other apps from yours unless you declare which ones you interact with. Without this declaration, Android can't find 1Password's activity and launching the request fails. Add a `<queries>` element to your app's `AndroidManifest.xml`:

```xml AndroidManifest.xml theme={null}
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <queries>
        <package android:name="com.onepassword.android" />
    </queries>

    <application>
        <!-- ... -->
    </application>
</manifest>
```

You don't need any permissions. 1Password's activity is exported for any app to start.

## Send the access request

1. Register `OnePasswordAccessRequestContract` with `registerForActivityResult` in your activity or fragment, before it's started.
2. Wrap the access request reference in an `OnePasswordAccessRequest`.
3. Launch the request when the person is ready to review it.

```kotlin theme={null}
import android.content.ActivityNotFoundException
import androidx.activity.ComponentActivity

class AgentActivity : ComponentActivity() {

    private val requestAccess = registerForActivityResult(OnePasswordAccessRequestContract()) { result ->
        // See "Handle the result".
    }

    fun presentAccessRequest(accessRequestReference: String) {
        try {
            requestAccess.launch(OnePasswordAccessRequest(accessRequestReference))
        } catch (e: ActivityNotFoundException) {
            // 1Password isn't installed, or this version doesn't support access requests.
        }
    }
}
```

Start the request for a result, as shown here. If you start it with `startActivity` instead, 1Password still shows the sheet, but your app never learns the person's decision.

> [!TIP]
>
> If launching throws `ActivityNotFoundException`, ask the person to install or update 1Password from Google Play, then try again.

## Handle the result

Android calls your activity result callback when the sheet closes. What `OnePasswordAccessRequestContract` returns depends on how the request ended:

| Outcome | Result code | `STATUS` extra | Result |
| - | - | - | - |
| The person granted access | `RESULT_OK` | `authorized` | `Granted`, with the access request status |
| The person denied the request | `RESULT_OK` | `denied` | `Denied` |
| 1Password couldn't show or answer the request, and the person closed the message | `RESULT_OK` | `error` | `Failed`, with the error |
| The person closed the sheet without deciding, or the system closed it | `RESULT_CANCELED` | None | `Cancelled` |

When 1Password can't show or answer the access request, the sheet tells the person why, and offers to try again when that can help. This includes a request with a missing or malformed extra. Your app gets the `error` result when the person closes that message, not straight away.

`Cancelled` means the access request is still pending, so the person can answer it later. The person can't close the sheet while 1Password sends their answer.

```kotlin theme={null}
private val requestAccess = registerForActivityResult(OnePasswordAccessRequestContract()) { result ->
    when (result) {
        is OnePasswordAccessRequestResult.Granted -> onGranted(decodeAccessGrant(result.accessRequestStatus))
        OnePasswordAccessRequestResult.Denied -> onDenied()
        is OnePasswordAccessRequestResult.Failed -> onFailed(result.error)
        OnePasswordAccessRequestResult.Cancelled -> onCancelled()
    }
}
```

### Decode the access grant

Decode the access request status as an `OnePasswordAccessGrant`. Ignore keys you don't recognize, because a later release may add optional fields without changing the protocol version:

```kotlin theme={null}
import kotlinx.serialization.json.Json

private val accessGrantJson = Json { ignoreUnknownKeys = true }

fun decodeAccessGrant(accessRequestStatus: String): OnePasswordAccessGrant =
    accessGrantJson.decodeFromString(accessRequestStatus)
```

### Use the grant

Each `reference` in the grant identifies one granted login. It holds no secret, and it works only with the access token and integration key of the connection that created the request, which stay on your side. The extension and the SDK both take the string inside it, `reference.reference`, not the object:

* **To fill in your agent's browser:** pass `reference.reference` to the extension's [`fillCredential`](/agentic-autofill/partners/fill#fill-a-granted-login) as `resourcePath`, with the same connection's access token and integration key.
* **To read the login with the SDK:** pass `reference.reference` to the SDK's read methods. See [Read a granted login](/agentic-autofill/partners/sdk.md#read-a-granted-login).

## Errors

If 1Password can't process the request, the result carries an error code in the `com.onepassword.android.workloadaccess.extra.ERROR` extra. These are the codes 1Password reports today, the cases of `OnePasswordAccessRequestError`:

| Error | Code | Meaning | What to do |
| - | - | - | - |
| `InvalidRequest` | `invalid_request` | A required extra is missing or malformed, or 1Password couldn't read the access request reference. | Check that the `Intent` carries both extras and that the reference is unchanged. |
| `UnsupportedVersion` | `unsupported_version` | This version of 1Password doesn't support the `PROTOCOL_VERSION` you sent, or the access request needs a newer version of 1Password. | Ask the person to update 1Password, then try again. |
| `AccountNotFound` | `account_not_found` | The account the access request belongs to isn't signed in to 1Password on this device. | Ask the person to add the account to 1Password, then try again without changing the access request. |
| `FeatureNotEnabled` | `feature_not_enabled` | Access requests aren't turned on for any account signed in to 1Password on this device. | Don't retry. The account's administrator has to turn them on. |
| `RequestUnavailable` | `request_unavailable` | 1Password couldn't find the access request for this person, or it was already granted, denied, or failed, for example on another device. | Create a new access request. |
| `RequestFailed` | `request_failed` | The person answered, but 1Password couldn't share the logins they chose, or couldn't send their answer. | Create a new access request. |
| `InternalError` | `internal_error` | 1Password failed unexpectedly, or couldn't connect. The access request is still pending. | Try again later without changing the access request. |

Treat any other code, or a `Failed` result with a `null` error, as a failed access request. 1Password may add error codes.

Unlike [on iOS](/agentic-autofill/partners/ios-approval.md#errors), a denial isn't an error. When the person denies the request, you get `Denied`. Don't retry automatically after `Denied` or `Cancelled`.

## Open an access request link instead

If your app can't start an activity for a result, for example because it runs in a browser, open the access request link instead, and add a `return_url` query parameter to it. 1Password opens `return_url` when the access request ends, which brings the person back to your app:

```
https://<server>/open/grant-access?return_url=<url-encoded URL>#access_request_reference=<reference>
```

Adding `return_url` is the only change you make to the link. Keep everything else as you got it.

`return_url` can be an App Link or a URL scheme your app handles. During the partner preview, 1Password opens any `return_url` the link carries. A later release opens only the return URLs registered for your OAuth client, so tell your 1Password contact which ones you use.

1Password adds the outcome to `return_url` as query parameters, and keeps any parameters your URL already has:

| Outcome | Parameters added |
| - | - |
| The person granted access | `access_request_status=authorized` |
| The person denied the request | `access_request_status=denied` |
| The access request ended on an error, and the person closed the message | `access_request_status=error&access_request_error=<code>` |

`<code>` is one of the [error codes](#errors) above, except `internal_error`, because an error that leaves the access request pending doesn't end it. The parameters never carry credentials, so read what the person granted with the SDK's [`accessRequests.getStatus`](/agentic-autofill/partners/sdk#wait-for-the-decision).

1Password doesn't open `return_url` when the person closes the sheet without answering, because the access request is still pending and they can answer it later. A link without `return_url` still works: when the request ends, 1Password closes the sheet, and Android shows the app the person was in before.

If the person opens the link with no account signed in to 1Password, the sheet offers to sign in, and your app gets `account_not_found`. 1Password doesn't hold on to the link while the person signs in, so they open it again afterward.

An app that starts the request for a result, as in [Send the access request](#send-the-access-request), isn't sent to `return_url`. It gets the outcome as the activity result.


## Related topics

- [Authorize SSH requests with 1Password](/ssh/agent/authorization.md)
- [Build integrations with 1Password](/get-started/build-integrations.md)
- [1Password SDK concepts](/sdks/concepts.md)
