> ## Documentation Index
> Fetch the complete documentation index at: https://docs.privy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Authorize intent

After an intent is proposed, it must be authorized before Privy executes it. Each eligible owner or signer authorizes the intent by adding an **authorization signature**. Signatures can be added independently, over time, until the resource's authorization threshold is met.

## Authorize an intent

To authorize an intent, submit an authorization signature to the authorize endpoint:

```sh theme={"system"}
POST https://api.privy.io/v1/intents/{intent_id}/authorize
```

The endpoint can be called with your app secret or with a wallet owner's user token, and accepts an object with the following fields:

<ParamField body="signature" type="string" required>
  An [authorization signature](/api-reference/authorization-signatures) over the intent's action.
</ParamField>

<ParamField body="timestamp" type="number" required>
  Unix timestamp, in milliseconds, when the signature was created. Privy uses it to verify the
  signing key was valid at signing time. Must match the `timestamp` in the [signature
  payload](#signature-payload).
</ParamField>

<Tabs>
  <Tab title="REST API">
    ```bash theme={"system"}
    curl -X POST https://api.privy.io/v1/intents/<intent_id>/authorize \
      -u "<your-privy-app-id>:<your-privy-app-secret>" \
      -H "privy-app-id: <your-privy-app-id>" \
      -H "Content-Type: application/json" \
      -d '{
        "signature": "<authorization-signature>",
        "timestamp": 1741834854578
      }'
    ```
  </Tab>

  <Tab title="Java SDK">
    ```java theme={"system"}
    import io.privy.api.PrivyClient;
    import io.privy.api.models.components.AuthorizationContext;
    import io.privy.api.models.operations.IntentAuthorizeResponse;
    import java.util.Arrays;

    PrivyClient client = PrivyClient.builder()
        .appId("your-privy-app-id")
        .appSecret("your-app-secret")
        .build();

    // The authorize endpoint accepts a single signature, so include exactly one signing mechanism.
    AuthorizationContext authorizationContext = AuthorizationContext.builder()
        .addAuthorizationPrivateKeys(Arrays.asList("your-authorization-private-key"))
        .build();

    // Fetches the intent, builds and signs the authorization payload, and submits it.
    IntentAuthorizeResponse response = client.intents().authorize("insert-intent-id", authorizationContext);
    ```
  </Tab>
</Tabs>

The `signature` is an [authorization signature](/api-reference/authorization-signatures) over the intent's underlying request, generated with the resource owner's authorization key. Intents use their own payload shape, described in [signature payload](#signature-payload) below.

<Tip>
  The authorize endpoint accepts a single signature per call. To satisfy a threshold greater than
  one, each owner or signer calls the endpoint with their own signature.
</Tip>

## Signature payload

An intent authorization signature covers the intent's **underlying action**, not the authorize request. The payload is the [standard signature payload](/controls/authorization-keys/using-owners/sign/overview#signature-payload) plus two fields specific to intents, `timestamp` and `intent_id`. Omitting either one fails verification with `Invalid signature`.

<Tip>
  Privy's SDKs build and sign this payload automatically. The fields below are only needed when
  [implementing signing
  directly](/controls/authorization-keys/using-owners/sign/direct-implementation).
</Tip>

Sign a JSON object with the following fields:

<ParamField body="version" type="1" required>
  Authorization signature version. Currently, `1` is the only version.
</ParamField>

<ParamField body="method" type="'POST' | 'PATCH' | 'DELETE'" required>
  HTTP method of the intent's underlying action, copied from `request_details.method`. This is not
  the method of the authorize request.
</ParamField>

<ParamField body="url" type="string" required>
  Full URL of the intent's underlying action, copied from `request_details.url` (e.g.
  `https://api.privy.io/v1/wallets/{wallet_id}/rpc`). This is not the URL of the authorize endpoint.
</ParamField>

<ParamField body="body" type="JSON" required>
  JSON body of the intent's underlying action, copied from `request_details.body`.
</ParamField>

<ParamField body="timestamp" type="number" required>
  Unix timestamp, in milliseconds, when the signature was created. Must match the `timestamp` sent
  in the authorize request body, and must be within 5 minutes of Privy's server time.
</ParamField>

<ParamField body="intent_id" type="string" required>
  ID of the intent being authorized. Binds the signature to a single intent so it cannot be replayed
  against another.
</ParamField>

<ParamField body="headers" type="object" required>
  JSON object containing only the headers below. Exclude all others, including
  `privy-idempotency-key`, which Privy does not include when verifying intent signatures.

  <Expandable title="headers properties">
    <ParamField body="headers.privy-app-id" type="string" required>
      Privy app ID.
    </ParamField>

    <ParamField body="headers.privy-request-expiry" type="string">
      The intent's `expires_at` value as a string. Include this only when the intent's
      `custom_expiry` is `true`.
    </ParamField>
  </Expandable>
</ParamField>

An example payload for an RPC intent:

```json theme={"system"}
{
  "version": 1,
  "method": "POST",
  "url": "https://api.privy.io/v1/wallets/xs76o3pi0v5syd62ui1wmijw/rpc",
  "body": {
    "method": "eth_sendTransaction",
    "caip2": "eip155:4217",
    "chain_type": "ethereum",
    "params": {
      "transaction": {
        "to": "0x0000000000000000000000000000000000000000",
        "value": 1
      }
    }
  },
  "timestamp": 1741834854578,
  "intent_id": "clpq1234567890abcdefghij",
  "headers": {
    "privy-app-id": "insert-your-app-id"
  }
}
```

Canonicalize the payload per [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785), sign it with ECDSA P-256, and base64-encode the signature, exactly as described in [implementing signing directly](/controls/authorization-keys/using-owners/sign/direct-implementation). Unlike other signed requests, the signature is submitted in the authorize request **body** rather than in a `privy-authorization-signature` header.

### Worked example

The example below fetches an intent, builds the payload from its `request_details`, signs it, and submits the authorization. Because Privy rejects a `timestamp` more than 5 minutes from its server time, build and sign the payload immediately before submitting it.

```typescript theme={"system"}
import canonicalize from 'canonicalize';
import crypto from 'crypto';

const PRIVY_APP_ID = 'insert-your-app-id';
const PRIVY_APP_SECRET = 'insert-your-app-secret';
const PRIVY_AUTHORIZATION_KEY = 'wallet-auth:insert-your-private-key-here';

const authHeaders = {
  Authorization: `Basic ${Buffer.from(`${PRIVY_APP_ID}:${PRIVY_APP_SECRET}`).toString('base64')}`,
  'privy-app-id': PRIVY_APP_ID,
  'Content-Type': 'application/json'
};

async function authorizeIntent(intentId: string) {
  // 1. Fetch the intent to read the underlying action and expiry details.
  const intent = await fetch(`https://api.privy.io/v1/intents/${intentId}`, {
    headers: authHeaders
  }).then((res) => res.json());

  // 2. Build the payload. `timestamp` must be within 5 minutes of Privy's server time.
  const timestamp = Date.now();
  const payload = {
    version: 1,
    method: intent.request_details.method,
    url: intent.request_details.url,
    body: intent.request_details.body,
    timestamp,
    intent_id: intentId,
    headers: {
      'privy-app-id': PRIVY_APP_ID,
      // Only required for intents proposed with a custom expiry.
      ...(intent.custom_expiry ? {'privy-request-expiry': String(intent.expires_at)} : {})
    }
  };

  // 3. Canonicalize per RFC 8785 and sign with ECDSA P-256.
  const serializedPayload = Buffer.from(canonicalize(payload) as string);
  const privateKey = crypto.createPrivateKey({
    key: `-----BEGIN PRIVATE KEY-----\n${PRIVY_AUTHORIZATION_KEY.replace('wallet-auth:', '')}\n-----END PRIVATE KEY-----`,
    format: 'pem'
  });
  const signature = crypto.sign('sha256', serializedPayload, privateKey).toString('base64');

  // 4. Submit the signature with the same timestamp used in the payload.
  return fetch(`https://api.privy.io/v1/intents/${intentId}/authorize`, {
    method: 'POST',
    headers: authHeaders,
    body: JSON.stringify({signature, timestamp})
  }).then((res) => res.json());
}
```

## Execution

When authorizations meet the resource's authorization threshold, Privy executes the action automatically. The intent moves from **Pending** to **Processing** for asynchronous actions such as transfers, and then to **Executed** or **Failed**.

Retrieve the outcome by [fetching the intent](/transaction-management/intents/fetch-intent) or by listening to the `intent.executed` webhook. For transaction intents, the `action_result` field contains the transaction hash. See [intent status](/transaction-management/intents/lifecycle) for details on each status.

## Idempotency

Privy records one authorization per signer. Re-submitting the same signer's authorization is safe: it does not add a duplicate approval or advance the intent past its threshold more than once, and the action executes only once when the threshold is met.

## Errors

Authorizing an intent returns an error in the following cases:

| Condition | Description |
| - | - |
| Intent not found | No intent matches the provided `intent_id`. |
| Intent not authorizable | The intent is already **Executed**, **Failed**, **Rejected**, **Expired**, **Dismissed**, or **Processing**. |
| Invalid signature | The `signature` is malformed, does not match the [signature payload](#signature-payload), or the `timestamp` falls outside the window in which the signing key was valid. |
| Timestamp out of range | The `timestamp` is more than 5 minutes from Privy's server time. |
| Ineligible signer | The signer is not an owner or signer eligible to authorize this intent. |

## API reference

<Card title="Authorize intent" icon="arrow-right" horizontal href="/api-reference/intents/authorize">
  View the full API reference for authorizing an intent.
</Card>

## Next steps

<CardGroup cols={2}>
  <Card title="Propose intents" icon="paper-plane" href="/transaction-management/intents/create/execute-transfer">
    Propose an intent to transfer funds, run a transaction, or update a resource.
  </Card>

  <Card title="Intent status" icon="arrows-spin" href="/transaction-management/intents/lifecycle">
    Track an intent from proposal to execution.
  </Card>

  <Card title="Reject intent" icon="ban" href="/transaction-management/intents/reject-intents">
    Cancel a pending intent before it is authorized and executed.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.