# Permit

Source: https://docs.settlemint.com/docs/concepts/modeling-assets/token-features/permit
Reference for the permit token feature and the Platform API routes that sign a permit, relay it, and read its signing domain and nonce.

The permit feature lets a token holder approve a spender with a signature instead of an approve transaction, and the Console and the Platform API sign, store and relay that signature.

A [token feature](https://docs.settlemint.com/docs/concepts/modeling-assets/token-features) is a module attached to one token. An account with the token's governance role attaches, detaches and configures it after issuance where the feature allows it, and each entry names any other role an action needs. The DALP Library is the catalogue of [instrument templates](https://docs.settlemint.com/docs/concepts/modeling-assets/instrument-templates) that ship with the platform, and each entry says which of them include the feature.

## Permit [#permit]

Lets a holder approve a spender with an off-chain signature instead of an approve transaction, following EIP-2612, the Ethereum standard for signed approvals. The signed message is a permit. It lets a spender, an address such as an exchange or a payment contract, move up to a set amount of the holder's tokens until a deadline. The amount the spender may move is its allowance, and the permit sets the allowance when someone submits it. Anyone can submit the signed permit. The feature checks the deadline, consumes the owner's nonce first, verifies the signature, and sets the allowance on the token. The nonce is a counter that the feature keeps for each owner. It starts at 0 and rises by one whenever one of the owner's permits is used, so a signature works once and only in order. The standard form accepts signatures from ordinary wallets. A second form checks signatures through EIP-1271, the standard that lets a contract wallet vouch for a signature, so a smart wallet can sign. The signature is bound to the token's name at deployment, and a second submission of the same signature fails because the nonce has moved. The feature restricts no token operation. Choose it when a spender should not need a separate approve transaction. The DALP Library includes it on 32 templates, all except Carbon Credit, Controlling Class Share, Custody receipt, and Share-Class Mirror.

The feature has no parameters. A holder signs a permit with "Create permit approval" or through the Platform API. The button sits on the permit page, which "View permit details" on the Permit card of the Features tab opens. The review step states "A Custodian relays this permit." On the chain anyone can submit a permit. On the platform an account with the custodian role relays it, from [Actions](https://docs.settlemint.com/docs/console/essentials/actions) or from the permit page, and the organisation pays the network fee, through its paymaster when it sponsors gas. Signing and relaying each ask for wallet verification. That is a second check, such as a PIN code or a one-time code, before the platform signs or submits on your behalf. A signed permit that nobody relays before its deadline expires, and the holder signs a new one. After issuance, governance attaches and detaches it. The Console attaches it from the "Attach a feature" list on the token's Features tab and detaches it behind a typed confirmation. It cannot be rotated. Attaching it again after a detach restores the same contract with its nonces unchanged, as the nonce section of the Permit API below describes. A permit with a passed deadline is rejected with `ERC2612ExpiredSignature`. It runs on EVM only. [Smart wallet](https://docs.settlemint.com/docs/concepts/custody-and-signing/smart-wallet) covers the signing side for a contract wallet.

### Permit API [#permit-api]

Two Platform API routes handle a permit. One signs it and stores it, and the other relays it so that the allowance changes on the chain. The [generated API reference](https://docs.settlemint.com/docs/api-reference/v2) lists the request bodies, the responses, and the error codes of both under "Sign and store a permit" and "Relay a signed permit".

| Route | Who calls it | What it does |
| --- | --- | --- |
| `POST /api/v2/tokens/{assetId}/features/permit/signatures` | The holder. The platform signs with the wallet that executes for the holder, after wallet verification, so the caller needs no token role | Signs a permit for the `spender`, `value`, and `deadline` in the body, stores it as `pending`, and returns it with its `id` and `nonce`. It sends no transaction |
| `POST /api/v2/tokens/{assetId}/features/permit/permits` | An account with the custodian role | Sets the allowance on the chain. The body names a stored permit by `signedPermitId`, or gives `owner`, `spender`, `value`, and `deadline` with either `v`, `r`, and `s` or one packed `signature` |
| `GET /api/v2/tokens/{assetId}/permit-info` | Any signed-in caller | Reads the feature address, the domain separator, the permit type hash, the token name, and, when you send `owner`, that owner's current nonce. It answers 200 with `data: null` when the token has no attached Permit feature |
| `GET /api/v2/tokens/{assetId}/features/permit/signatures` | Any signed-in caller | Lists stored permits with their `status` (`pending`, `relayed`, or `expired`) and `relayable` |
| `GET /api/v2/tokens/{assetId}/permit/replay-history` | Any signed-in caller | Lists the nonces that were already used on the chain |

Amounts such as `value` are whole numbers in the token's smallest unit, sent as strings or integers up to the largest `uint256`. A `deadline` is a Unix timestamp in seconds. Both write routes take a `walletVerification` object with a `secretVerificationCode` and a `verificationType` of `PINCODE`, `OTP`, `SECRET_CODES`, or `OIDC_MFA`. A permit reaches the chain in one of two ways. In the platform-signed path, the holder calls the sign route and a custodian then relays the stored permit by `signedPermitId`. In the externally signed path, your signer reads `permit-info`, signs the typed data itself, and a custodian relays the signature with `owner`, `spender`, `value`, `deadline`, and either `v`, `r`, `s` or `signature`. For a contract wallet, send the bytes that the wallet's EIP-1271 check accepts as `signature`. The relay route answers 202 with a `transactionId`, a `status`, and a `statusUrl` by default. Send `Prefer: wait=<seconds>` to wait for settlement and receive 200 with the token. The platform keeps `wait` between 5 and 99 seconds. [Idempotency and async settlement](https://docs.settlemint.com/docs/api-reference/guides/idempotency-async-settlement) covers the 202 handle and the `Prefer` header.

```bash
curl -X POST "https://your-instance.example.com/api/v2/tokens/0x7a1c0b9e5d3f4a8b2c6e1d9f0a3b5c7d9e1f2a4b/features/permit/signatures" \
  -H "X-Api-Key: sm_dalp_..." \
  -H "Content-Type: application/json" \
  -d '{
    "spender": "0x3f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be",
    "value": "500000000000000000000",
    "deadline": 1793750400,
    "walletVerification": { "verificationType": "PINCODE", "secretVerificationCode": "123456" }
  }'
```

The response holds the stored permit. Its `id` is the value to relay.

```json
{
  "data": {
    "id": "0198a3c4-5b7e-7d2a-9c41-6f0e8b3d5a12",
    "tokenAddress": "0x7a1c0b9e5d3f4a8b2c6e1d9f0a3b5c7d9e1f2a4b",
    "owner": "0x9d4e2b6a8c0f1e3d5b7a9c2e4f6a8b0d1c3e5f7a",
    "spender": "0x3f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be",
    "value": "500000000000000000000",
    "deadline": "1793750400",
    "nonce": "3",
    "signatureKind": "ecdsa",
    "status": "pending",
    "createdAt": "2026-10-28T13:05:11.000Z"
  },
  "links": { "self": "/api/v2/tokens/0x7a1c0b9e5d3f4a8b2c6e1d9f0a3b5c7d9e1f2a4b/features/permit/signatures" }
}
```

A custodian relays that permit and waits up to 30 seconds for the result.

```bash
curl -X POST "https://your-instance.example.com/api/v2/tokens/0x7a1c0b9e5d3f4a8b2c6e1d9f0a3b5c7d9e1f2a4b/features/permit/permits" \
  -H "X-Api-Key: sm_dalp_..." \
  -H "Prefer: wait=30" \
  -H "Content-Type: application/json" \
  -d '{
    "signedPermitId": "0198a3c4-5b7e-7d2a-9c41-6f0e8b3d5a12",
    "walletVerification": { "verificationType": "PINCODE", "secretVerificationCode": "123456" }
  }'
```

#### The signing domain [#the-signing-domain]

A permit signature follows EIP-712, the standard for signing structured data. The domain a signer must use has four fields, and the permit feature contract answers for them.

| Domain field | Value |
| --- | --- |
| `name` | The token's name when the permit feature was deployed |
| `version` | `1` |
| `chainId` | The chain the token runs on |
| `verifyingContract` | The permit feature contract, which `permit-info` returns as `featureAddress` |

The signed type is `Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)`. `permit-info` returns the token's current indexed name as `tokenName`. If your hash does not match `domainSeparator`, the name at deployment differs, and `domainSeparator` is the value to trust. `permit-info` also returns the hash of the domain as `domainSeparator` and the hash of the type as `permitTypeHash`, so you can compare them with what your signer computes before you sign. The domain separator is indexed and `asOfBlockNumber` says which block it was read at. The nonce is read live. The sign route compares the domain that the feature declares with the indexed separator and stops with `TOKEN_PERMIT_FEATURE_UNAVAILABLE` when they differ, and with `TOKEN_PERMIT_READ_REFUSED` when the separator is not indexed yet.

#### Nonces [#nonces]

Use the nonce from `permit-info` when you build a signature yourself. The chain accepts a permit only when its nonce equals the owner's current nonce, and the use of the permit raises that nonce by one.

- **Signing through the platform.** The sign route starts at the owner's current nonce and skips every nonce that a pending stored permit uses. Two permits from one holder therefore get consecutive nonces. A permit whose nonce is above the current nonce stays `pending` with `relayable: false` until the permits before it are used. An expired permit does not advance the nonce: sign a new permit, which takes the released nonce, and relay it first.
- **A permit held for custody approval.** When a custody approval rule, which sends the signature request to a named approver before it is signed, holds the signature, the sign route answers 409 `TOKEN_PERMIT_SIGNING_PENDING` and keeps the nonce for that permit. Another permit from the same holder takes the next free nonce. After the approver approves, sign again with the same spender, value, and deadline to receive the signature over the held nonce. If a different permit claims the nonce in the meantime, the losing call answers 409 `TOKEN_PERMIT_NONCE_CONFLICT`, and you sign again.
- **Detaching and attaching Permit again.** Detaching Permit removes it from the token's list and leaves its contract in place. Attaching it again restores that same contract, at the same address, so the nonces keep their values and the signing domain does not change. While Permit is detached, the sign and relay routes refuse every request because the token has no attached Permit feature, and a stored permit cannot be relayed. After the new attach, a stored permit whose deadline has not passed and whose nonce equals the owner's current nonce relays again, and a signature that you built outside the platform stays valid on the same terms. A permit whose deadline passes while Permit is detached expires as usual.

#### Retries and idempotency [#retries-and-idempotency]

- **Sign.** The sign route does not deduplicate. Each successful call stores one more permit with the next free nonce, so a retry after a lost response creates a second permit. List the stored permits before you retry. A permit that waits for custody approval is the exception: the same spender, value, and deadline receive the same nonce.
- **Relay by `signedPermitId`.** The platform deduplicates by the stored permit and not by the `Idempotency-Key` header. While a relay of that permit is queued or running, a repeat call answers 202 with the `transactionId` of the first relay and sends no second transaction. After the relay settled, a repeat call answers 409 `TOKEN_PERMIT_ALREADY_RELAYED`, or 409 `TOKEN_PERMIT_NOT_RELAYABLE` when the platform has not recorded the result and the nonce has moved. Read the permit's status either way. When the first relay failed, the next call submits a new transaction.
- **Relay with a signature.** No stored permit exists, so the header does the work. Send the same `Idempotency-Key` on every retry, and the transaction queue attaches the retry to the first request. The queue matches the retry to the first request, and the route itself gives no replay guarantee. Without the header the platform generates a new key for each call, a retry sends a second transaction, and the chain rejects it because the nonce is already used.

#### Errors [#errors]

A request with a missing or wrong wallet verification, a caller without the custodian role on the relay route, and a token without an attached Permit feature use the platform's standard errors. Those need a change to the request or the caller before any retry, except a wallet verification attempt limit (429), which clears after the wait that the response states. The permit-specific errors are below. The [error reference](https://docs.settlemint.com/docs/api-reference/errors) lists every code with its fix.

| Code | Status | Retry | Route | Cause |
| --- | --- | --- | --- | --- |
| `TOKEN_PERMIT_DEADLINE_EXPIRED`, DALP-9495 | 422 | No | Sign | The deadline is not after the current chain time. Sign again with a later deadline |
| `TOKEN_PERMIT_WALLET_UNAVAILABLE`, DALP-9496 | 409 | No | Sign | No executor wallet, the organisation wallet that signs and submits on a user's behalf, is set for this organisation and chain. Configure one |
| `TOKEN_PERMIT_NOT_FOUND`, DALP-9497 | 404 | No | Relay | No stored permit matches this organisation, chain, token, and `signedPermitId` |
| `TOKEN_PERMIT_ALREADY_RELAYED`, DALP-9498 | 409 | No | Relay | The permit was already submitted. Read the permit and its transaction instead |
| `TOKEN_PERMIT_NOT_RELAYABLE`, DALP-9499 | 409 | No | Relay | The permit expired, its deadline passed, or its nonce is not the owner's current nonce, for example because the chain already used it. Read the permit, then sign a new one if needed |
| `TOKEN_PERMIT_NONCE_CONFLICT`, DALP-9500 | 409 | Yes | Sign | Another permit took the nonce while this one was stored. Sign again |
| `TOKEN_PERMIT_FEATURE_UNAVAILABLE`, DALP-9501 | 503 | Yes | Sign | The owner's nonce or the token name is not available, or the domain that the feature declares does not match the indexed one. Retry after indexing |
| `TOKEN_PERMIT_SIGNING_FAILED`, DALP-9502 | 503 | Yes | Sign | The custody signer did not complete the signature |
| `TOKEN_FEATURE_PERSISTENCE_FAILED`, DALP-9266 | 503 | Yes | Both | The platform could not read or save the token feature data that the request needs |
| `TOKEN_PERMIT_PERSISTENCE_FAILED`, DALP-9505 | 503 | Yes | Both | The platform could not read or save the stored permit. Retry without changing the request |
| `TOKEN_PERMIT_SIGNING_PENDING`, DALP-14037 | 409 | No | Sign | A custody approval holds the signature. The response includes the approval reference and its expiry, which is never later than the permit deadline |
| `TOKEN_PERMIT_CUSTODY_APPROVAL_CLOSED`, DALP-14038 | 409 | No | Sign | The custody provider rejected the signature, it failed, or the approval expired. Sign a new permit |
| `TOKEN_PERMIT_READ_REFUSED`, DALP-13001 | 422 | No | Both | A nonce or domain read failed for the reason in the response, for example a domain separator that is not indexed yet. Report it with the request ID if it persists |
| `CONTRACT_ERROR`, DALP-9026, DALP-9027, DALP-9069 | 422 | No | Relay | The chain rejected a permit sent with a signature: the deadline passed (9026), the recovered signer is not the owner (9027), or a contract wallet's signature failed verification (9069). Rebuild the signature with the current nonce and domain, or sign a new permit with a later deadline |
