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

# Signing delegation

> Let users grant your server time-limited access to sign within one Request, from a React Native app

In a React Native app, you open the grant page in an in-app browser, and the Para Portal sends the user's decision back to your app through a deep link. Your server then signs while the user is away.

## What signing delegation does

A Request lets your app sign a kind of transaction without a prompt, but only while the user is signed in to your app. Signing delegation lets your server sign for a user after their session ends, for recurring buys, subscriptions or top-ups.

* The user grants one version of one Request on one wallet, until an expiry they choose. The expiry cannot be later than the maximum you set on the Request, and the maximum cannot be more than 90 days.
* While the grant is active, your backend signs with that wallet through the REST API and your secret API key. It stores no user session and shows no prompt.
* Para checks every request against your active policies. A request signs only when a rule in the granted Request allows it. Para refuses every other request with `403` and the code `SIGNING_GRANT_REQUIRED`, and sends nothing to the user for review.
* The user can revoke the grant at any time. The grant also ends at its expiry, and when that version of the Request stops being active.

| | Signing in the user's session | Signing delegation |
| - | - | - |
| Who signs | Your app, while the user is signed in, or your server with a session the user's app exported to it | Your server, with your secret API key and the wallet ID |
| What the user approves | The Request, on the consent screen | The Request and an expiry, on the grant page |
| A request no rule allows | Goes to the user for review | Refused with `403 SIGNING_GRANT_REQUIRED` |
| A rule that requires approval | Para opens an approval or review | Refused with `403 SIGNING_GRANT_REQUIRED`. Para opens nothing. |
| When it ends | When the session ends | At the expiry, on revocation, when the Request version stops being active, or when the user changes their adjustable values for it |

A grant and a consent answer are separate. A grant covers only what your server signs with your secret API key, and the user's consent answer still decides what your app signs in their session.

## Before you start

Signing delegation needs Permissions on your API key, a wallet that can hold a grant, and a client app where the user is signed in.

* Permissions must be on for your API key: it needs at least one active policy, and its app needs a project. Otherwise, signing on a user-owned wallet returns `403 FORBIDDEN`.
* The wallet must be created under your API key, and Para must hold its share. That is the case when Para keeps the user's shares in its enclave, or when your app created the wallet, or migrated its share, through the REST API before a user claimed it. For any other wallet, the grant page shows **Signing while you are away is not available for this wallet yet.**, and your app receives the error code `SIGNING_GRANT_WALLET_NOT_ELIGIBLE`.
* The user grants access from a JavaScript client: the web SDKs, the React SDKs or the React Native SDK. The Swift and Flutter SDKs cannot open the grant page yet.
* Your server signs with your secret API key, through the REST API.

## Turn it on for a Request

Signing delegation is a setting on a Request. It is off by default. Turning it on lets users grant access, and does not give your server access by itself.

> [tab] Developer Portal
>
> 1. On your **Development** key, open **Permissions**, select the **Requests** tab, and select **Add** or **New Request**.
> 2. Write the Request as usual. On the policy step, turn on **Allow signing while the user is away**.
> 3. In **Maximum signing grant duration**, enter a number and choose **seconds**, **minutes**, **hours** or **days**. Users can choose a shorter period. The maximum is 90 days.
> 4. Select **Review**. The review shows **Signing while away** as **Enabled**, with the **Maximum Grant Duration**. Select **Save Draft**, then **Activate** on the draft.

> [tab] JSON
>
> Set `metadata.signingDelegation.maxDurationSeconds` to a positive whole number of seconds, at most `7776000` (90 days):
>
> ```json theme={null}
> "metadata": {
>   "productMode": "delegation",
>   "description": "Invest up to 50 USDC a week from your wallet on Base",
>   "signingDelegation": { "maxDurationSeconds": 7776000 }
> }
> ```
>
> A larger value fails validation at `$.metadata.signingDelegation.maxDurationSeconds`. `signingDelegation` on a Guardrail fails with `signing_delegation_mode_invalid`.

A Request with signing delegation goes through the same [production review](https://docs.getpara.com/v3/concepts/permissions.md#production) as any other policy.

> [!WARNING]
>
> Activating an edited version of a Request, or turning the Request off, ends every grant on the old version at once. Your server cannot sign for those users until each of them grants the active version.
>
> Turning the Request back on does not bring the old grants back. Before you turn off a Request that allows signing delegation, the Developer Portal asks you to confirm with **Turn off and stop grants**.

## How a grant works

The user grants access on a Para Portal page that your app opens. Para records one grant for the user, wallet, Request version and expiry, under the API key your app used. Only that API key's secret can sign under the grant.

| Status | Meaning | Can your server sign? |
| - | - | - |
| `active` | The grant has not reached its expiry, and its Request version is still active. | Yes, for requests the Request allows |
| `expired` | The grant reached its expiry. The user can reauthorize it while the same Request version is active. | No |
| `revoked` | The user revoked the grant. It cannot be renewed; the user grants again instead. | No |
| `superseded` | A newer grant for the same Request replaced it, after a renewal, a reauthorization or a new grant. | No. The newer grant applies. |
| `invalidated` | The Request version is no longer active, because you activated a new version, turned it off or its time window ended. It is also invalidated when the user changes their adjustable values for that Request. | No |

### Expiry and renewal

The user sets the expiry on the grant page, and can extend it later without waiting for it to end.

* The grant page suggests the Request's maximum, or the `requestedExpiresAt` your app passes, capped at the maximum. The user can choose any time between now and the maximum.
* The maximum counts from the moment of granting. With a 90-day maximum, a grant made on 1 March can last until 30 May.
* **Renew** replaces an active grant with a new one, whose expiry counts from the renewal. Para marks the old grant `superseded`.
* The user can reauthorize an expired grant with a new expiry, as long as the same Request version is still active.
* A wallet holds at most one active grant for each Request. A user who already has one sees **Continue with existing access** and **Renew signing access** instead of a new grant.

### When a grant ends

A grant ends when it expires, when the user revokes it, when its Request version stops being active, or when the user changes their adjustable values for that Request.

> [!NOTE]
>
> Para checks the grant again just before the wallet starts signing. A request that has not reached that check when the grant ends is refused. A request that has already passed it can still complete.

> [!NOTE]
>
> Your server has no route that lists a user's grants. Store the `expiresAt` your app receives when the user grants access, and remind the user to renew before it. When signing returns `403 FORBIDDEN`, treat the grant as ended.

## What the user sees

Your app opens the grant page in the Para Portal. The user signs in to the Portal if they need to.

**1. Review the access**

The page asks **Allow  to act while you are away?** and shows the wallet and your Request's description. It explains: " can request signatures without asking you to return or sign in again, but only for the authority shown below." It lists any adjustable values, the **Maximum duration** and the Request's rules under **See Details**.

**2. Choose an expiry**

The **Allow access until** field holds the suggested expiry. The user can move it earlier, but not past the maximum.

**3. Decide**

The user selects **Allow signing while I am away** or **Not now**. The page notes: "You can revoke this access at any time. Replacing or turning off the policy also stops it."

The page changes when the user already has a grant for the Request:

| Situation | What the page shows |
| - | - |
| Active grant | "Existing access is active until .", with **Continue with existing access**, **Renew signing access** and **Revoke access** |
| Expired grant | "This access expired. Choose a new expiry to reauthorize it.", with **Reauthorize signing while I am away** |
| The user saved adjustable values for an earlier version of the Request | "This permission changed. Review it before you allow signing.", with **Review permission**, which opens the consent screen |
| The version your app asked for is no longer active | "This signing policy is no longer available. Ask the app to start a new request." |

Under **Your signing grants**, the page lists the user's other grants to your app on this wallet with their status. An active grant offers **Renew** and **Revoke**, and an expired one **Reauthorize**.

To let users review or revoke access later, open the same page without a Request. It is titled **Apps that can sign while you are away** and lists only the grants. Give users a button for it in your app's settings. Para hosts no other page where the user can revoke a grant.

## How Para decides delegated requests

When your server signs a user-owned wallet with your secret API key, Para evaluates the request against your active policies, as for any other request. The result decides the response:

| Evaluation result | Response |
| - | - |
| A rule in the granted Request allows the request | Signs |
| The request is allowed, but not by the granted Request | `403 SIGNING_GRANT_REQUIRED`, with the reason `outside-signing-grant` |
| No rule in your policies covers it, so it would go to the user for review | `403 SIGNING_GRANT_REQUIRED`. `deniedBy` names Para's default review rule, such as `review-user-owned` with the reason `matched:para:review-user-owned`, or has the reason `default-deny` when no rule decides. |
| A rule requires approval, or a rule denies it | `403 SIGNING_GRANT_REQUIRED`, with the deciding rule in `deniedBy`. Para opens no review and no approval. |
| The wallet has no active grant | `403 FORBIDDEN` |

### Considerations for your other policies

A delegated request goes through the same evaluation as any other request, with a few differences that affect how you write your policies.

* Only the granted Request authorizes a delegated request. Other Requests the user accepted in your app, including with standing access, do not let your server sign, and their rules, including approval rules, do not apply. Put every limit you rely on in the granted Request.
* A matching approval rule in the granted Request refuses the request instead of opening an approval. Keep approval tiers in Requests that users sign in your app.
* A transaction that performs several actions, such as a Safe batch, signs only when one granted Request allows each of them.
* Para's defaults still apply. Para refuses a request it cannot read, or one that a rule cannot be checked against. Para skips a rule that depends on a detail it cannot decode, so that rule cannot allow the request either.
* Guardrails govern app-owned wallets, so they do not apply to a granted wallet, which belongs to a user.
* A `wallet.spend.*` budget belongs to the wallet and your API key. Transfers the user signs in your app and transfers your server signs under the grant count toward the same total.
* A grant records the values the user had set for the Request's adjustable parameters. When the user changes them, the grant ends and the user grants again.

> [!WARNING]
>
> While grants are active, your secret API key can sign for those users' wallets within the granted Requests. Keep it on your server, and limit where it works with an [IP allowlist](https://docs.getpara.com/v3/rest/setup.md#ip-allowlisting).

## Scenarios

Each automation pairs Request rules that cap what your server signs with a maximum duration that fits how often the user should renew.

| Scenario | Request rules | Maximum duration | What your server does |
| - | - | - | - |
| Recurring investing | A USDC permit to your broker's order contract, or a USDC transfer to its deposit address, up to 50 USDC a week | 90 days | A weekly job signs the order's permit or sends the transfer, then places the order with your broker. |
| Subscription | A token transfer to your billing address, up to the plan price | 90 days | A billing job charges the user each period, and asks them to renew before the grant expires. |
| Balance top-up | A transfer to the user's card or game balance, with a daily budget | 30 days | When the balance falls below a threshold, your server tops it up. |
| Trading session | Calls to your router contract, with a token approval capped at the session budget | 1 hour | An agent places trades for one session, and the grant ends with it. |

### Recurring investing

A user agrees to invest 50 USDC a week. Each week, your backend places an order with your broker for them. The user grants access once and renews at most every 90 days. Which rule you write depends on how the broker takes payment.

#### The broker takes an ERC-2612 permit

Many brokers take each order as a USDC permit that lets their order contract pull the payment. Para reads the permit's token, amount and spender, so a Request can allow permits only to that contract, up to 50 USDC each and 50 USDC a week:

```json request-weekly-permit-investing.json theme={null}
{
  "id": "base-weekly-permit-investing",
  "schemaVersion": "permissions.policy.v1",
  "version": "1",
  "selector": {
    "walletType": "EVM",
    "chainId": "8453"
  },
  "rules": [
    {
      "id": "allow-weekly-usdc-order-permit",
      "effect": "allow",
      "actions": ["sign_typed_data"],
      "when": {
        "kind": "all",
        "of": [
          { "kind": "compare", "fact": "request.typedData.primaryType", "op": "eq", "value": "Permit" },
          {
            "kind": "compare",
            "fact": "request.token.asset",
            "op": "eq",
            "value": "eip155:8453:token:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
          },
          {
            "kind": "compare",
            "fact": "request.token.spender",
            "op": "eq",
            "value": "0x4444444444444444444444444444444444444444"
          },
          { "kind": "compare", "fact": "request.token.value.baseUnits", "op": "lte", "value": "50000000" },
          {
            "kind": "aggregate",
            "fact": "wallet.spend.erc20",
            "op": "lte",
            "value": "50000000",
            "window": "168h",
            "asset": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
          }
        ]
      }
    }
  ],
  "metadata": {
    "productMode": "delegation",
    "description": "Invest up to 50 USDC a week from your wallet on Base",
    "signingDelegation": {
      "maxDurationSeconds": 7776000
    }
  }
}
```

| Signing request from your server | Result |
| - | - |
| A 50 USDC permit to the order contract, first of the week | Signs, and counts 50 USDC toward the week |
| A second 50 USDC permit in the same week | `403 SIGNING_GRANT_REQUIRED`, because no rule allows it |
| A permit to any other spender, or for another token | `403 SIGNING_GRANT_REQUIRED`, because no rule allows it |
| Any request after the user revoked the grant, or after it expired | `403 FORBIDDEN` |

> [!NOTE]
>
> Para reads a permit only when it is an ERC-2612 `Permit` signed as typed data: `primaryType` is `Permit`, the type lists exactly `owner` and `spender` as `address`, then `value`, `nonce` and `deadline` as `uint256`, and the domain sets `chainId` and the token as `verifyingContract`. Para counts a permit toward the budget only when `owner` is the signing wallet.
>
> Other permit formats, such as DAI-style permits and Permit2, have no `request.token.*` facts, so this rule does not allow them. The budget counts the permit's `value` when your server signs it, not what the spender later pulls, so include the broker's fees in the amount.

> [!WARNING]
>
> A signed permit stays usable on-chain until its `deadline`, even after the user revokes the grant. A Request rule cannot limit the deadline, so set a short one when your server signs.

#### The broker takes a transfer

If the broker funds orders from a deposit address instead, allow USDC transfers to that address:

```json request-weekly-usdc-investing.json theme={null}
{
  "id": "base-weekly-usdc-investing",
  "schemaVersion": "permissions.policy.v1",
  "version": "1",
  "selector": {
    "walletType": "EVM",
    "chainId": "8453"
  },
  "rules": [
    {
      "id": "allow-weekly-usdc-deposit",
      "effect": "allow",
      "actions": ["transfer_tokens"],
      "when": {
        "kind": "all",
        "of": [
          { "kind": "compare", "fact": "request.classification", "op": "eq", "value": "erc20_transfer" },
          {
            "kind": "compare",
            "fact": "request.contract.address",
            "op": "eq",
            "value": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
          },
          { "kind": "compare", "fact": "request.method", "op": "eq", "value": "transfer" },
          {
            "kind": "compare",
            "fact": "request.to",
            "op": "eq",
            "value": "0x1111111111111111111111111111111111111111"
          },
          { "kind": "compare", "fact": "request.token.value.baseUnits", "op": "lte", "value": "50000000" },
          {
            "kind": "aggregate",
            "fact": "wallet.spend.erc20",
            "op": "lte",
            "value": "50000000",
            "window": "168h",
            "asset": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
          }
        ]
      }
    }
  ],
  "metadata": {
    "productMode": "delegation",
    "description": "Invest up to 50 USDC a week from your wallet on Base",
    "signingDelegation": {
      "maxDurationSeconds": 7776000
    }
  }
}
```

| Signing request from your server | Result |
| - | - |
| Transfer 50 USDC to the deposit address, first transfer of the week | Signs |
| A second 50 USDC transfer in the same week | `403 SIGNING_GRANT_REQUIRED`, because no rule allows it |
| Transfer 50 USDC to any other address | `403 SIGNING_GRANT_REQUIRED`, because no rule allows it |

> [!TIP]
>
> The `168h` window resets every Thursday at 00:00 UTC. Schedule the job so it runs once in each window.

## Ask the user for access

Build the link with `getSigningDelegationConsentUrl`, open it with an auth session that waits for your deep link, and read the result with `parseSigningDelegationConsentOutcome`. `appId` is your API key's project ID, from `para.partnerRecord.projectId`.

> [!NOTE]
>
> React Native has no popup, so `requestSigningDelegation` and `useSigningDelegation` do not work there.

> [!WARNING]
>
> `returnUrl` must use your app's custom URL scheme. The Portal rejects an `http` or `https` return link with **This signing delegation return link is invalid.** after it saves the user's decision, so the grant changes but your app receives no outcome.

```typescript signing-access.ts theme={null}
import { parseSigningDelegationConsentOutcome } from "@getpara/react-native-wallet";
import { openAuthSessionAsync } from "expo-web-browser";
import { para } from "./para";

const RETURN_URL = "myapp://signing-delegation";

export async function requestSigningAccess(walletId: string, policyId: string) {
  const appId = para.partnerRecord?.projectId;
  if (!appId) throw new Error("Para has not loaded your project yet");

  const { policy } = await para.getApplicablePolicy(policyId, { walletId });
  const url = await para.getSigningDelegationConsentUrl({
    appId,
    walletId,
    policyId,
    policyVersion: policy.policyVersion,
    policyHash: policy.policyHash,
    returnUrl: RETURN_URL,
  });

  const result = await openAuthSessionAsync(url, RETURN_URL);
  if (result.type !== "success") return { status: "cancelled" } as const;
  return parseSigningDelegationConsentOutcome(result.url) ?? ({ status: "cancelled" } as const);
}
```

The outcome has the same shape as on the web:

| `status` | Meaning |
| - | - |
| `granted` | The user granted, renewed or kept access. `expiresAt` is the grant's expiry. Send it to your server. |
| `revoked` | The user revoked the grant this request was about. |
| `cancelled` | The user selected **Not now**. Treat a closed browser the same way. |
| `error` | `errorCode` is `SIGNING_GRANT_WALLET_NOT_ELIGIBLE` when the wallet cannot hold a grant. |

To suggest a shorter expiry, pass `requestedExpiresAt` as an ISO 8601 timestamp. The page caps it at the Request's maximum.

## Let users manage access

Build the link with only `appId`, `walletId` and `returnUrl` to open the page that lists the user's grants to your app. Renewing or revoking there does not return to your app, so the user closes the browser when they are done, and the result does not tell you what changed. Your server learns about a revocation from the `403 FORBIDDEN` on its next signing request.

```typescript theme={null}
const url = await para.getSigningDelegationConsentUrl({ appId, walletId, returnUrl: RETURN_URL });
await openAuthSessionAsync(url, RETURN_URL);
```

## Sign from your server

Once the user has granted access, your backend signs with their wallet ID and your secret API key, through the same REST routes as for app-owned wallets: `sign-transaction`, `sign-message`, `sign-typed-data`, `sign-raw`, `sign-authorization` and `transfer`. You do not pass a grant ID. Para finds the active grant for the wallet on each request.

```typescript weekly-buy.ts theme={null}
import { ParaRestClient, ParaRestError } from "@getpara/rest-sdk";

const para = new ParaRestClient({ apiKey: process.env.PARA_SECRET_API_KEY!, env: "BETA" });

const USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
const DEPOSIT_ADDRESS = "0x1111111111111111111111111111111111111111";

export async function runWeeklyBuy(plan: { id: string; walletId: string; week: string }) {
  try {
    const transfer = await para.transfer(
      plan.walletId,
      { chainId: "8453", to: DEPOSIT_ADDRESS, value: "50000000", tokenAddress: USDC_BASE },
      { idempotencyKey: `weekly-buy-${plan.id}-${plan.week}` },
    );
    return { status: "sent", transfer } as const;
  } catch (error) {
    if (!(error instanceof ParaRestError)) throw error;
    if (error.code === "FORBIDDEN") {
      return { status: "grant-ended" } as const;
    }
    if (error.code === "SIGNING_GRANT_REQUIRED") {
      return { status: "outside-grant", deniedBy: (error.body as { deniedBy?: unknown }).deniedBy } as const;
    }
    throw error;
  }
}
```

Handle each refusal in your job:

| Response | Meaning | What to do |
| - | - | - |
| `403 FORBIDDEN`, `REST API signing on a user-owned wallet requires an active signing grant` | The wallet has no active grant. It expired, the user revoked it, the Request version changed, or the user changed their adjustable values for the Request. | Stop the schedule, and ask the user to grant access again in your app. |
| `403 SIGNING_GRANT_REQUIRED`, `signing grant does not allow this request` | The grant is active, but the granted Request does not allow this request. `deniedBy` lists the reasons. | Do not resend the same request. Fix the request, or ask the user to sign it in your app. |
| `403 SIGNING_GRANT_REQUIRED`, `signing grant is not active` | The grant ended while Para processed the request. | Treat it as `FORBIDDEN`. |
| `409 CONFLICT` | Another Request that the user granted on this wallet changed while Para processed the request: its adjustable values or its active version. | Send the request again with a new `Idempotency-Key`. |

> [!TIP]
>
> Send an `Idempotency-Key` with every scheduled request, built from the schedule and the period, so a retried job cannot sign the same payment twice.

> [!NOTE]
>
> Gas sponsorship (`sponsoredGas`), automatic retries (`autoRetry`) and `estimate-fee` work only with app-owned wallets. On a user-owned wallet they return `403`, even with an active grant.

### Sign a permit

For a broker that takes permits, sign each order's permit with `signTypedData` and send the signature to the broker. Para reads the permit's token, amount and spender from the typed data:

```typescript order-permit.ts theme={null}
import { ParaRestClient } from "@getpara/rest-sdk";

const para = new ParaRestClient({ apiKey: process.env.PARA_SECRET_API_KEY!, env: "BETA" });

const USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
const ORDER_CONTRACT = "0x4444444444444444444444444444444444444444";

export async function signOrderPermit(plan: {
  id: string;
  walletId: string;
  address: string;
  week: string;
  nonce: string;
  deadline: string;
}) {
  const { signature } = await para.signTypedData(
    plan.walletId,
    {
      typedData: {
        domain: { name: "USD Coin", version: "2", chainId: 8453, verifyingContract: USDC_BASE },
        types: {
          Permit: [
            { name: "owner", type: "address" },
            { name: "spender", type: "address" },
            { name: "value", type: "uint256" },
            { name: "nonce", type: "uint256" },
            { name: "deadline", type: "uint256" },
          ],
        },
        primaryType: "Permit",
        message: { owner: plan.address, spender: ORDER_CONTRACT, value: "50000000", nonce: plan.nonce, deadline: plan.deadline },
      },
    },
    { idempotencyKey: `weekly-permit-${plan.id}-${plan.week}` },
  );
  return signature;
}
```

Read `nonce` from the token's `nonces(owner)` before you sign, and keep `deadline` short. A permit outside the grant throws a `ParaRestError` with the code `SIGNING_GRANT_REQUIRED`.

### Sign with viem

`createParaDelegatedViemAccount` from `@getpara/rest-sdk/viem` returns a viem account that signs messages and typed data through the REST API with your secret key. To reuse a client you already created, pass `client` instead of `apiKey` and `env`.

```typescript delegated-account.ts theme={null}
import { createParaDelegatedViemAccount } from "@getpara/rest-sdk/viem";

export function delegatedAccount(walletId: string, address: `0x${string}`) {
  return createParaDelegatedViemAccount({
    apiKey: process.env.PARA_SECRET_API_KEY!,
    env: "BETA",
    walletId,
    address,
  });
}
```

> [!NOTE]
>
> The viem account sends no `Idempotency-Key`, so sign scheduled payments with the REST client instead. On Base and other OP Stack chains, viem passes the chain's own transaction serializer, which the account rejects. Send transactions on those chains with the REST client's `transfer` or `signTransaction`.


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