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

# Message-Level-Encryption (MLE)

> End-to-end encryption for sensitive data transmission

<Note>
  This page documents two message level encryption modes.

  * **Standard MLE** is available to every team. It encrypts the request and response bodies with a `Method-MLE: jwe` header and an `{"encrypted": "..."}` envelope.
  * **[Signed MLE](#signed-message-level-encryption)** is optional and opt-in on top of standard MLE. It signs the payload before encrypting it, which authenticates the sender in both directions. To enable it, contact your Method representative.

  If signed MLE has not been enabled for your team, the standard path documented below applies to you.
</Note>

## What is Message Level Encryption?

Message Level Encryption (MLE) provides end-to-end encryption for sensitive data transmitted between your application and Method's API. Using a hybrid encryption approach, MLE ensures your data remains protected even if network traffic is intercepted.

MLE uses two layers of encryption:

* **Symmetric encryption** (AES-GCM) encrypts your actual data payload using a Content Encryption Key (CEK)
* **Asymmetric encryption** (RSA-OAEP-256) encrypts the CEK using Method's public key

This approach combines the efficiency of symmetric encryption with the security of public-key cryptography.

## Prerequisites

To use MLE with Method's API, you'll need:

* An RSA key pair for RSA-OAEP-256
* Ability to create and parse JWE (JSON Web Encryption) in compact serialization format

MLE requests require:

* Header: `Method-MLE: jwe`
* Content-Type: `application/json`
* Request body: `{"encrypted": "<compact dot separated JWE string>"}`

## Setup Guide

### Step 1: Generate Your RSA Key Pair

Generate an RSA key pair that will be used to receive encrypted responses from Method.

<CodeGroup>
  ```javascript Node.js theme={null}
  import { generateKeyPair, exportJWK } from 'jose';

  // Generate RSA key pair
  const { publicKey, privateKey } = await generateKeyPair('RSA-OAEP-256', {
    modulusLength: 2048,
  });

  // Export as JWK format
  const publicJwk = await exportJWK(publicKey);
  const privateJwk = await exportJWK(privateKey);

  // Add required fields to your public JWK
  publicJwk.alg = 'RSA-OAEP-256';
  publicJwk.use = 'enc';
  publicJwk.kid = 'your-unique-key-id'; // Choose a unique identifier

  // Store privateJwk securely (e.g., in your key management system)
  console.log('Public JWK:', publicJwk);
  ```

  ```python Python theme={null}
  from jwcrypto import jwk
  import json

  # Generate RSA key pair
  key = jwk.JWK.generate(kty='RSA', size=2048, alg='RSA-OAEP-256', use='enc')

  # Add a unique key ID
  key.kid = 'your-unique-key-id'

  # Export public and private keys
  public_jwk = json.loads(key.export_public())
  private_jwk = json.loads(key.export_private())

  # Store private_jwk securely
  print('Public JWK:', public_jwk)
  ```
</CodeGroup>

### Step 2: Register Your Public Key with Method

You can register your public key using either a well-known endpoint (recommended) or direct registration.

<Note>
  **Important**: Each key ID (`kid`) can only be registered once using either method. If you have a public key available through your well-known endpoint, you should not register the same public key through direct registration, even if you change the `kid`.
</Note>

#### Option A: Well-Known Endpoint (Recommended)

Host your public JWK at a well-known URL and register it with Method:

<CodeGroup>
  ```javascript Node.js theme={null}
  const response = await fetch('https://production.methodfi.com/teams/mle/public_keys', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk_your_token',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      type: 'well_known',
      contact: 'security@yourcompany.com',
      well_known_endpoint: 'https://api.yourcompany.com/.well-known/jwks.json'
    })
  });

  const result = await response.json();
  console.log('Registration result:', result);
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://production.methodfi.com/teams/mle/public_keys',
      headers={
          'Authorization': 'Bearer sk_your_token',
          'Content-Type': 'application/json',
      },
      json={
          'type': 'well_known',
          'contact': 'security@yourcompany.com',
          'well_known_endpoint': 'https://api.yourcompany.com/.well-known/jwks.json'
      }
  )

  print('Registration result:', response.json())
  ```
</CodeGroup>

Your well-known endpoint should return:

```json theme={null}
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "your-unique-key-id",
      "use": "enc",
      "alg": "RSA-OAEP-256",
      "n": "...",
      "e": "AQAB"
    }
  ]
}
```

### Requirements for keys on `.well-known` endpoint

1. Must have a top-level field named `keys` that has a list as its value.
2. For a JWK (an item in list of `keys`) to be valid the following must be met:
   1. JWK must be an object
   2. JWK must have a field named `kty` and it must be equal to `RSA`
   3. JWK must have a field `n` and it must be a string that is valid `n` for a JWK in accordance with the RFC
   4. JWK must have a field `e` and it must be a string that is valid `e` for a JWK in accordance with the RFC
   5. JWK can optionally have a field named `alg` but if it is provided the value must match the key's `use`: `RSA-OAEP-256` for `enc`, or `RS256` for `sig`
   6. JWK must have a field `kid` and it must be a string that is a valid `id`; on the standard MLE path this is the value you pass as `cid` when making requests to Method
   7. JWK can optionally have a field named `use`; it defaults to `enc`, and `sig` is accepted for signing keys on the [signed MLE path](#signed-message-level-encryption)

#### Option B: Direct Registration

Alternatively, register your public key directly:

<CodeGroup>
  ```javascript Node.js theme={null}
  const response = await fetch('https://production.methodfi.com/teams/mle/public_keys', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk_your_token',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      type: 'direct',
      contact: 'security@yourcompany.com',
      jwk: publicJwk // Your public JWK from Step 1
    })
  });
  ```

  ```python Python theme={null}
  response = requests.post(
      'https://production.methodfi.com/teams/mle/public_keys',
      headers={
          'Authorization': 'Bearer sk_your_token',
          'Content-Type': 'application/json',
      },
      json={
          'type': 'direct',
          'contact': 'security@yourcompany.com',
          'jwk': public_jwk  # Your public JWK from Step 1
      }
  )
  ```
</CodeGroup>

### Step 3: Retrieve Method's Public Key

Fetch Method's public key for encrypting your requests:

<CodeGroup>
  ```javascript Node.js theme={null}
  const response = await fetch('https://production.methodfi.com/.well-known/jwks.json', {
    headers: {
      'Authorization': 'Bearer sk_your_token'
    }
  });

  const { keys } = await response.json();
  // Select an active encryption key
  const methodPublicKey = keys.find(k => k.use === 'enc' && k.status === 'active');
  console.log('Method public key:', methodPublicKey);
  ```

  ```python Python theme={null}
  response = requests.get(
      'https://production.methodfi.com/.well-known/jwks.json',
      headers={'Authorization': 'Bearer sk_your_token'}
  )

  keys = response.json()['keys']
  # Select an active encryption key
  method_public_key = next(k for k in keys if k['use'] == 'enc' and k['status'] == 'active')
  print('Method public key:', method_public_key)
  ```
</CodeGroup>

<Note>
  Method's JWKS also publishes signing keys, which carry `use: "sig"` and `alg: "RS256"` and are used only on the [signed MLE path](#signed-message-level-encryption). Filter on `use` as well as `status` when selecting a key for encryption.
</Note>

<Note>
  Method's public keys are environment-specific:

  * Production: `https://production.methodfi.com/.well-known/jwks.json`
  * Sandbox: `https://sandbox.methodfi.com/.well-known/jwks.json`
  * Development: `https://dev.methodfi.com/.well-known/jwks.json`
</Note>

## Making Encrypted Requests

### Step 1: Encrypt Your Request Payload

<CodeGroup>
  ```javascript Node.js theme={null}
  import { CompactEncrypt, importJWK } from 'jose';

  async function encryptForMethod(payload, methodPublicJwk, yourKeyId) {
    // Convert payload to bytes
    const encoder = new TextEncoder();
    const data = encoder.encode(JSON.stringify(payload));

    // Import Method's public key
    const methodPublicKey = await importJWK(methodPublicJwk, 'RSA-OAEP-256');

    // Create JWE
    const jwe = await new CompactEncrypt(data)
      .setProtectedHeader({
        alg: 'RSA-OAEP-256',
        enc: 'A256GCM',
        kid: methodPublicJwk.kid,  // Method's key ID
        cid: yourKeyId,            // Your key ID for response encryption
        typ: 'JWE'
      })
      .encrypt(methodPublicKey);

    return jwe;
  }

  // Example: Encrypt entity data
  const entityData = {
    type: 'individual',
    individual: {
      first_name: 'Kevin',
      last_name: 'Doyle',
      phone: '+16505551234',
      dob: '1997-03-18',
      ssn_4: '1111',
      address: {
        line1: '3300 N Interstate 35',
        city: 'Austin',
        state: 'TX',
        zip: '78705'
      }
    }
  };

  const encryptedJwe = await encryptForMethod(entityData, methodPublicKey, 'your-unique-key-id');
  ```

  ```python Python theme={null}
  from jwcrypto import jwe, jwk
  import json

  def encrypt_for_method(payload, method_public_jwk, your_key_id):
      # Import Method's public key
      method_key = jwk.JWK(**method_public_jwk)

      # Create JWE
      jwe_token = jwe.JWE(
          json.dumps(payload),
          recipient=method_key,
          protected=json.dumps({
              'alg': 'RSA-OAEP-256',
              'enc': 'A256GCM',
              'kid': method_public_jwk['kid'],  # Method's key ID
              'cid': your_key_id,               # Your key ID
              'typ': 'JWE'
          })
      )

      # Return compact serialization
      return jwe_token.serialize(compact=True)

  # Example: Encrypt entity data
  entity_data = {
      'type': 'individual',
      'individual': {
          'first_name': 'Kevin',
          'last_name': 'Doyle',
          'phone': '+16505551234',
          'dob': '1997-03-18',
          'ssn': '111223333',
          'address': {
              'line1': '3300 N Interstate 35',
              'city': 'Austin',
              'state': 'TX',
              'zip': '78705'
          }
      }
  }

  encrypted_jwe = encrypt_for_method(entity_data, method_public_key, 'your-unique-key-id')
  ```
</CodeGroup>

### Step 2: Send the Encrypted Request

<CodeGroup>
  ```javascript Node.js theme={null}
  const response = await fetch('https://production.methodfi.com/entities', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk_your_token',
      'Content-Type': 'application/json',
      'Method-MLE': 'jwe'  // Required for MLE
    },
    body: JSON.stringify({
      encrypted: encryptedJwe
    })
  });

  // Response will also be encrypted
  const encryptedResponse = await response.json();
  ```

  ```python Python theme={null}
  response = requests.post(
      'https://production.methodfi.com/entities',
      headers={
          'Authorization': 'Bearer sk_your_token',
          'Content-Type': 'application/json',
          'Method-MLE': 'jwe'  # Required for MLE
      },
      json={
          'encrypted': encrypted_jwe
      }
  )

  # Response will also be encrypted
  encrypted_response = response.json()
  ```
</CodeGroup>

### Step 3: Decrypt the Response

<CodeGroup>
  ```javascript Node.js theme={null}
  import { compactDecrypt, importJWK } from 'jose';

  async function decryptFromMethod(encryptedJwe, yourPrivateJwk, expectedCid) {
    // Import your private key
    const privateKey = await importJWK(yourPrivateJwk, 'RSA-OAEP-256');

    // Decrypt the JWE
    const { plaintext, protectedHeader } = await compactDecrypt(
      encryptedJwe,
      privateKey
    );

    // Verify the response is encrypted with your key
    if (protectedHeader.kid !== expectedCid) {
      throw new Error(`Unexpected key ID: ${protectedHeader.kid}`);
    }

    // Parse and return the decrypted data
    const decoder = new TextDecoder();
    return JSON.parse(decoder.decode(plaintext));
  }

  // Decrypt the response
  const decryptedData = await decryptFromMethod(
    encryptedResponse.encrypted,
    yourPrivateJwk,
    'your-unique-key-id'
  );
  console.log('Decrypted response:', decryptedData);
  ```

  ```python Python theme={null}
  from jwcrypto import jwe, jwk
  import json

  def decrypt_from_method(encrypted_jwe, your_private_jwk, expected_cid):
      # Import your private key
      private_key = jwk.JWK(**your_private_jwk)

      # Parse and decrypt the JWE
      jwe_token = jwe.JWE()
      jwe_token.deserialize(encrypted_jwe)
      jwe_token.decrypt(private_key)

      # Verify the response is encrypted with your key
      protected_header = json.loads(jwe_token.jose_header)
      if protected_header['kid'] != expected_cid:
          raise ValueError(f"Unexpected key ID: {protected_header['kid']}")

      # Return the decrypted data
      return json.loads(jwe_token.payload)

  # Decrypt the response
  decrypted_data = decrypt_from_method(
      encrypted_response['encrypted'],
      private_jwk,
      'your-unique-key-id'
  )
  print('Decrypted response:', decrypted_data)
  ```
</CodeGroup>

## Complete Example

Here's a complete example showing the full MLE flow:

<CodeGroup>
  ```javascript Node.js theme={null}
  import { generateKeyPair, exportJWK, CompactEncrypt, compactDecrypt, importJWK } from 'jose';

  async function mleExample() {
    // 1. Generate your key pair (one-time setup)
    const { publicKey, privateKey } = await generateKeyPair('RSA-OAEP-256');
    const publicJwk = await exportJWK(publicKey);
    const privateJwk = await exportJWK(privateKey);

    publicJwk.alg = 'RSA-OAEP-256';
    publicJwk.use = 'enc';
    publicJwk.kid = 'my-key-2024';

    // 2. Register your public key with Method
    await fetch('https://production.methodfi.com/teams/mle/public_keys', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer sk_your_token',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        type: 'direct',
        contact: 'security@example.com',
        jwk: publicJwk
      })
    });

    // 3. Get Method's public key
    const methodKeysResponse = await fetch('https://production.methodfi.com/.well-known/jwks.json', {
      headers: { 'Authorization': 'Bearer sk_your_token' }
    });
    const { keys } = await methodKeysResponse.json();
    const methodPublicKey = keys.find(k => k.use === 'enc' && k.status === 'active');

    // 4. Encrypt your request
    const payload = {
      type: 'individual',
      individual: {
        first_name: 'Kevin',
        last_name: 'Doyle',
        ssn: '111223333'
      }
    };

    const methodKey = await importJWK(methodPublicKey, 'RSA-OAEP-256');
    const encryptedJwe = await new CompactEncrypt(
      new TextEncoder().encode(JSON.stringify(payload))
    )
      .setProtectedHeader({
        alg: 'RSA-OAEP-256',
        enc: 'A256GCM',
        kid: methodPublicKey.kid,
        cid: 'my-key-2024',
        typ: 'JWE'
      })
      .encrypt(methodKey);

    // 5. Send encrypted request
    const response = await fetch('https://production.methodfi.com/entities', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer sk_your_token',
        'Content-Type': 'application/json',
        'Method-MLE': 'jwe'
      },
      body: JSON.stringify({ encrypted: encryptedJwe })
    });

    // 6. Decrypt response
    const { encrypted } = await response.json();
    const { plaintext } = await compactDecrypt(encrypted, await importJWK(privateJwk));
    const result = JSON.parse(new TextDecoder().decode(plaintext));

    console.log('Created entity:', result);
  }
  ```

  ```python Python theme={null}
  from jwcrypto import jwk, jwe
  import json
  import requests

  def mle_example():
      # 1. Generate your key pair (one-time setup)
      key = jwk.JWK.generate(kty='RSA', size=2048, alg='RSA-OAEP-256', use='enc')
      key.kid = 'my-key-2024'

      public_jwk = json.loads(key.export_public())
      private_jwk = json.loads(key.export_private())

      # 2. Register your public key with Method
      requests.post(
          'https://production.methodfi.com/teams/mle/public_keys',
          headers={
              'Authorization': 'Bearer sk_your_token',
              'Content-Type': 'application/json',
          },
          json={
              'type': 'direct',
              'contact': 'security@example.com',
              'jwk': public_jwk
          }
      )

      # 3. Get Method's public key
      response = requests.get(
          'https://production.methodfi.com/.well-known/jwks.json',
          headers={'Authorization': 'Bearer sk_your_token'}
      )
      keys = response.json()['keys']
      method_public_key = next(k for k in keys if k['use'] == 'enc' and k['status'] == 'active')

      # 4. Encrypt your request
      payload = {
          'type': 'individual',
          'individual': {
              'first_name': 'Kevin',
              'last_name': 'Doyle',
              'ssn': '111223333'
          }
      }

      method_key = jwk.JWK(**method_public_key)
      jwe_token = jwe.JWE(
          json.dumps(payload),
          recipient=method_key,
          protected=json.dumps({
              'alg': 'RSA-OAEP-256',
              'enc': 'A256GCM',
              'kid': method_public_key['kid'],
              'cid': 'my-key-2024',
              'typ': 'JWE'
          })
      )
      encrypted_jwe = jwe_token.serialize(compact=True)

      # 5. Send encrypted request
      response = requests.post(
          'https://production.methodfi.com/entities',
          headers={
              'Authorization': 'Bearer sk_your_token',
              'Content-Type': 'application/json',
              'Method-MLE': 'jwe'
          },
          json={'encrypted': encrypted_jwe}
      )

      # 6. Decrypt response
      encrypted_response = response.json()['encrypted']
      response_jwe = jwe.JWE()
      response_jwe.deserialize(encrypted_response)
      response_jwe.decrypt(key)

      result = json.loads(response_jwe.payload)
      print('Created entity:', result)
  ```
</CodeGroup>

## Signed Message Level Encryption

Signed Message Level Encryption (signed MLE) adds sender authentication to standard MLE. Requests are signed with your private signing key before they are encrypted, and responses are signed by Method before they are encrypted to your public encryption key.

Signed MLE uses nested JOSE tokens:

* The request or response payload is signed as a JWS using RS256
* The signed JWS is encrypted as a JWE using RSA-OAEP-256 and AES-GCM

<Note>
  Signed MLE is optional and is not enabled by default. To enable it, contact your Method representative.
</Note>

### Prerequisites

To use signed MLE, you'll need:

* An RSA encryption key pair for RSA-OAEP-256
* An RSA signing key pair for RS256
* Ability to create and parse compact JWS and JWE tokens
* The customer and Method identifiers provided during setup

Signed MLE requests require:

* Content-Type: `application/jose`
* Request body: a compact JWE string containing a signed JWS

The `Method-MLE: jwe` header and `{"encrypted": "..."}` wrapper used by standard MLE are not used for signed MLE.

### Setup Guide

#### Step 1: Generate Your Signing Key Pair

Generate a separate RSA key pair for signing requests.

<CodeGroup>
  ```javascript Node.js theme={null}
  import { generateKeyPair, exportJWK } from 'jose';

  const { publicKey, privateKey } = await generateKeyPair('RS256', {
    modulusLength: 2048,
  });

  const publicJwk = await exportJWK(publicKey);
  const privateJwk = await exportJWK(privateKey);

  publicJwk.alg = 'RS256';
  publicJwk.use = 'sig';
  publicJwk.kid = 'your-signing-key-id';
  ```

  ```python Python theme={null}
  from jwcrypto import jwk
  import json

  key = jwk.JWK.generate(kty='RSA', size=2048, alg='RS256', use='sig')
  key.kid = 'your-signing-key-id'

  public_jwk = json.loads(key.export_public())
  private_jwk = json.loads(key.export_private())
  ```
</CodeGroup>

Store your private signing key securely. Only register the public key with Method.

#### Step 2: Register Your Signing Public Key with Method

Register your signing public key through the same MLE public keys endpoint used for encryption keys. The signing JWK must include:

```json theme={null}
{
  "kty": "RSA",
  "kid": "your-signing-key-id",
  "use": "sig",
  "alg": "RS256",
  "n": "...",
  "e": "AQAB"
}
```

You can register the key directly or include it in your well-known JWKS endpoint. See the [Create MLE Public Key](/2026-03-30/reference/teams/mle/create) reference for the accepted `use` and `alg` values and for the well-known requirements that apply to signing keys.

<Note>
  Signed MLE requests carry no `cid`, so Method selects the response encryption key itself: among your active `use: "enc"` registrations that are within their `nbf` and `exp` window, the one with the highest `iat` wins. Register an `iat` on every encryption key if more than one will ever be active at a time. Without it the choice is ambiguous and the request fails with `MLE_ENCRYPTION_KEY_UNAVAILABLE`.
</Note>

#### Step 3: Retrieve Method's Public Keys

Method publishes both encryption and signing keys from the same JWKS endpoint. Select keys using the `use` field:

<CodeGroup>
  ```javascript Node.js theme={null}
  const response = await fetch('https://production.methodfi.com/.well-known/jwks.json');
  const { keys } = await response.json();

  const methodEncryptionKey = keys.find(
    key => key.use === 'enc' && key.status === 'active'
  );

  const methodSigningKeys = keys.filter(
    key => key.use === 'sig' && key.status === 'active'
  );
  ```

  ```python Python theme={null}
  response = requests.get('https://production.methodfi.com/.well-known/jwks.json')
  keys = response.json()['keys']

  method_encryption_key = next(
      key for key in keys
      if key['use'] == 'enc' and key['status'] == 'active'
  )

  method_signing_keys = [
      key for key in keys
      if key['use'] == 'sig' and key['status'] == 'active'
  ]
  ```
</CodeGroup>

Use an active encryption key to encrypt requests to Method. Retain every active signing key: more than one can be active during a rotation, and the response names the one that signed it.

## Making Signed Encrypted Requests

### Step 1: Sign Your Request Payload

Create a JWT claims object containing your request data, then sign it as a compact JWS using your private RS256 signing key.

The signed payload includes:

| Claim | Description |
| - | - |
| `iss` | Your customer identifier |
| `aud` | Method's identifier |
| `nbf` | Time when the request becomes valid, in seconds since epoch |
| `exp` | Expiration time, in seconds since epoch |
| `data` | The Method API request body |

You can also include standard JWT claims such as `iat` and `jti`.

<Warning>
  The identifiers invert between directions. On requests you send, your customer identifier is `iss` and Method's identifier is `aud`. On responses Method returns, the two are reversed.
</Warning>

<CodeGroup>
  ```javascript Node.js theme={null}
  import { CompactSign, importJWK } from 'jose';
  import { randomUUID } from 'node:crypto';

  async function signPayload(payload, signingPrivateJwk, signingKeyId) {
    const now = Math.floor(Date.now() / 1000);
    const signingKey = await importJWK(signingPrivateJwk, 'RS256');

    const claims = {
      iss: 'your-customer-identifier',
      aud: 'method-identifier',
      iat: now,
      nbf: now,
      exp: now + 300,
      jti: randomUUID(),
      data: payload,
    };

    return new CompactSign(
      new TextEncoder().encode(JSON.stringify(claims))
    )
      .setProtectedHeader({
        alg: 'RS256',
        typ: 'JWT',
        kid: signingKeyId,
      })
      .sign(signingKey);
  }
  ```

  ```python Python theme={null}
  from jwcrypto import jws, jwk
  import json
  import time
  import uuid

  def sign_payload(payload, signing_private_jwk, signing_key_id):
      now = int(time.time())
      signing_key = jwk.JWK(**signing_private_jwk)

      claims = {
          'iss': 'your-customer-identifier',
          'aud': 'method-identifier',
          'iat': now,
          'nbf': now,
          'exp': now + 300,
          'jti': str(uuid.uuid4()),
          'data': payload,
      }

      token = jws.JWS(json.dumps(claims))
      token.add_signature(
          signing_key,
          None,
          json.dumps({
              'alg': 'RS256',
              'typ': 'JWT',
              'kid': signing_key_id,
          }),
      )

      return token.serialize(compact=True)
  ```
</CodeGroup>

### Step 2: Encrypt the Signed Payload

Encrypt the compact JWS using Method's active encryption key. Set `cty` to `JWT` to indicate that the encrypted content is a signed JWT.

<CodeGroup>
  ```javascript Node.js theme={null}
  import { CompactEncrypt, importJWK } from 'jose';

  async function encryptSignedPayload(signedPayload, methodPublicJwk) {
    const methodPublicKey = await importJWK(methodPublicJwk, 'RSA-OAEP-256');

    return new CompactEncrypt(
      new TextEncoder().encode(signedPayload)
    )
      .setProtectedHeader({
        alg: 'RSA-OAEP-256',
        enc: 'A256GCM',
        kid: methodPublicJwk.kid,
        cty: 'JWT',
      })
      .encrypt(methodPublicKey);
  }
  ```

  ```python Python theme={null}
  from jwcrypto import jwe, jwk
  import json

  def encrypt_signed_payload(signed_payload, method_public_jwk):
      method_key = jwk.JWK(**method_public_jwk)

      token = jwe.JWE(
          signed_payload,
          recipient=method_key,
          protected=json.dumps({
              'alg': 'RSA-OAEP-256',
              'enc': 'A256GCM',
              'kid': method_public_jwk['kid'],
              'cty': 'JWT',
          }),
      )

      return token.serialize(compact=True)
  ```
</CodeGroup>

### Step 3: Send the Signed Encrypted Request

Send the compact JWE directly as the request body. Set the `Method-Version` header as described in [Versioning](/2026-03-30/reference/versioning); without it, the request uses your team's default API version.

<CodeGroup>
  ```javascript Node.js theme={null}
  const response = await fetch('https://production.methodfi.com/entities', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk_your_token',
      'Content-Type': 'application/jose',
    },
    body: encryptedJwe,
  });
  ```

  ```python Python theme={null}
  response = requests.post(
      'https://production.methodfi.com/entities',
      headers={
          'Authorization': 'Bearer sk_your_token',
          'Content-Type': 'application/jose',
      },
      data=encrypted_jwe,
  )
  ```
</CodeGroup>

## Processing Signed Encrypted Responses

Signed MLE responses use the same nested format: a signed JWS encrypted inside a JWE.

To process a response:

1. Decrypt the JWE using the private encryption key identified by the JWE `kid`
2. Read the inner JWS protected header and select Method's signing key with the matching `kid`
3. Verify the RS256 signature
4. Validate the response claims
5. Read the Method API response from the `data` claim

On responses, the identifiers are reversed:

* `iss` is Method's identifier
* `aud` is your customer identifier

<CodeGroup>
  ```javascript Node.js theme={null}
  import {
    compactDecrypt,
    compactVerify,
    decodeProtectedHeader,
    importJWK,
  } from 'jose';

  const CUSTOMER_ID = 'your-customer-identifier';
  const METHOD_ID = 'method-identifier';

  // Tolerance applied when checking the response time bounds.
  const CLOCK_SKEW_SECONDS = 60;

  async function decryptAndVerifyResponse(
    encryptedResponse,
    encryptionPrivateJwks,
    methodSigningJwks
  ) {
    const jweHeader = decodeProtectedHeader(encryptedResponse);
    if (jweHeader.cty !== 'JWT') {
      throw new Error('Expected a nested JWS in the response.');
    }

    // Method chooses which of your encryption keys to use, so hold them by kid.
    const encryptionPrivateJwk = encryptionPrivateJwks[jweHeader.kid];
    if (!encryptionPrivateJwk) {
      throw new Error('Response encrypted to an unregistered key.');
    }
    const encryptionPrivateKey = await importJWK(
      encryptionPrivateJwk,
      'RSA-OAEP-256'
    );

    const { plaintext } = await compactDecrypt(
      encryptedResponse,
      encryptionPrivateKey
    );

    const signedResponse = new TextDecoder().decode(plaintext);
    const jwsHeader = decodeProtectedHeader(signedResponse);
    const methodSigningJwk = methodSigningJwks.find(
      key => key.kid === jwsHeader.kid
    );
    if (!methodSigningJwk) {
      throw new Error(`Unknown signing kid ${jwsHeader.kid}; refresh Method's JWKS.`);
    }
    const methodSigningKey = await importJWK(methodSigningJwk, 'RS256');

    // Pin the algorithm and require typ "JWT" so a token minted in another
    // JOSE context cannot be accepted as a signed MLE response.
    const { payload, protectedHeader } = await compactVerify(
      signedResponse,
      methodSigningKey,
      { algorithms: ['RS256'] }
    );
    if (protectedHeader.typ !== 'JWT') {
      throw new Error('Unexpected JWS type on the response.');
    }

    const claims = JSON.parse(new TextDecoder().decode(payload));

    if (claims.iss !== METHOD_ID) throw new Error('Unexpected issuer on the response.');
    if (claims.aud !== CUSTOMER_ID) throw new Error('Unexpected audience on the response.');

    // Method signs responses with a 300-second lifetime. Enforce both bounds.
    const now = Math.floor(Date.now() / 1000);
    if (typeof claims.nbf !== 'number' || claims.nbf > now + CLOCK_SKEW_SECONDS) {
      throw new Error('Response is not yet valid.');
    }
    if (typeof claims.exp !== 'number' || claims.exp <= now - CLOCK_SKEW_SECONDS) {
      throw new Error('Response has expired.');
    }

    return claims.data;
  }
  ```

  ```python Python theme={null}
  from jwcrypto import jwe, jws, jwk
  import base64
  import json
  import time

  CUSTOMER_ID = 'your-customer-identifier'
  METHOD_ID = 'method-identifier'

  # Tolerance applied when checking the response time bounds.
  CLOCK_SKEW_SECONDS = 60


  def decode_header(token):
      segment = token.split('.')[0]
      segment += '=' * (-len(segment) % 4)
      return json.loads(base64.urlsafe_b64decode(segment))


  def decrypt_and_verify_response(
      encrypted_response,
      encryption_private_jwks,
      method_signing_jwks,
  ):
      jwe_header = decode_header(encrypted_response)
      if jwe_header.get('cty') != 'JWT':
          raise RuntimeError('Expected a nested JWS in the response.')

      # Method chooses which of your encryption keys to use, so hold them by kid.
      encryption_private_jwk = encryption_private_jwks.get(jwe_header.get('kid'))
      if encryption_private_jwk is None:
          raise RuntimeError('Response encrypted to an unregistered key.')
      encryption_key = jwk.JWK(**encryption_private_jwk)

      encrypted = jwe.JWE()
      encrypted.deserialize(encrypted_response)
      encrypted.decrypt(encryption_key)

      signed_response = encrypted.payload.decode('utf-8')
      jws_header = decode_header(signed_response)
      # Require typ "JWT" so a token minted in another JOSE context cannot be
      # accepted as a signed MLE response.
      if jws_header.get('typ') != 'JWT':
          raise RuntimeError('Unexpected JWS type on the response.')

      method_signing_jwk = next(
          (key for key in method_signing_jwks if key['kid'] == jws_header.get('kid')),
          None,
      )
      if method_signing_jwk is None:
          raise RuntimeError(
              f"Unknown signing kid {jws_header.get('kid')}; refresh Method's JWKS."
          )

      verified = jws.JWS()
      verified.deserialize(signed_response)
      verified.verify(jwk.JWK(**method_signing_jwk), alg='RS256')

      claims = json.loads(verified.payload)

      if claims['iss'] != METHOD_ID:
          raise RuntimeError('Unexpected issuer on the response.')
      if claims['aud'] != CUSTOMER_ID:
          raise RuntimeError('Unexpected audience on the response.')

      # Method signs responses with a 300-second lifetime. Enforce both bounds.
      now = int(time.time())
      if not isinstance(claims.get('nbf'), (int, float)) or claims['nbf'] > now + CLOCK_SKEW_SECONDS:
          raise RuntimeError('Response is not yet valid.')
      if not isinstance(claims.get('exp'), (int, float)) or claims['exp'] <= now - CLOCK_SKEW_SECONDS:
          raise RuntimeError('Response has expired.')

      return claims['data']
  ```
</CodeGroup>

<Note>
  Signed MLE responses use `Content-Type: application/jose`, including errors raised after your request has been decrypted and verified. Decrypt and verify those the same way; the HTTP status is preserved. Errors raised before that point, such as a malformed JWE or a rejected signature, are returned as a standard JSON error. Branch on the response `Content-Type`.
</Note>

## Error Handling

When using MLE, you may encounter these specific error codes:

| **Error Type Code** | **Error Subtype Code** | **HTTP Status** | **Description** |
| - | - | - | - |
| `api_error` | `MLE_DECRYPTION_FAILED` | 500 | Message level encryption (MLE) requests are temporarily unavailable. To ensure MLE try your request later, or fall back to a non-MLE request. |
| `api_error` | `MLE_ENCRYPTION_FAILED` | 500 | Message level encryption (MLE) requests are temporarily unavailable. To ensure MLE try your request later, or fall back to a non-MLE request. |
| `api_error` | `PAYLOAD_INVALID_JSON` | 400 | The request body is not valid JSON. |
| `invalid_request` | `MLE_INVALID_HEADER` | 400 | When using message level encryption the header 'Method-MLE' must be set to 'jwe'. |
| `invalid_request` | `MLE_MISSING_ENCRYPTED_PAYLOAD` | 400 | When using message level encryption the payload must contain field 'encrypted' |
| `invalid_request` | `MLE_UNSUPPORTED_KEY_MANAGEMENT_ALGORITHM` | 400 | Key management algorithm provided is not supported. Must use 'RSA-OAEP-256' |
| `invalid_request` | `MLE_INVALID_ENCRYPTION_ALGORITHM` | 400 | Unsupported or missing "enc" in protected header. Expected one of: 'A256GCM' or 'A128GCM'. |
| `invalid_request` | `MLE_MUST_INCLUDE_KID` | 400 | Must include 'KID' in protected header. |
| `invalid_request` | `MLE_MUST_INCLUDE_CID` | 400 | Must include 'CID' in protected header. |
| `invalid_request` | `MLE_INVALID_KID` | 400 | The JWK KID is either disabled or does not exist. |
| `invalid_request` | `MLE_INVALID_CID` | 400 | The JWK CID is either disabled or does not exist. |
| `invalid_request` | `MLE_INVALID_JWE_FORMAT` | 400 | The MLE "encrypted" payload must be in the RFC compatible compact format. |
| `invalid_request` | `WELL_KNOWN_ENDPOINT_ALREADY_EXISTS` | 400 | An active well-known endpoint already exist. Must delete already existing well-known endpoint to post a new one. |
| `invalid_request` | `JWK_KID_ALREADY_EXISTS` | 400 | The specified JWK KID already exists. |
| `invalid_request` | `JWK_ALREADY_DISABLED` | 400 | The specified JWK is already disabled. |
| `invalid_request` | `JWK_ALREADY_EXISTS` | 400 | The specified JWK's public content matches another key you have posted. Based on thumbprint from `n` , `e` , `kty` . |
| `invalid_request` | `INVALID_JWK` | 400 | The specified JWK is invalid — for example, the `alg` does not match the `use`, or the key material cannot be imported. |
| `invalid_request` | `MLE_MISSING_CTY` | 400 | Must include `cty` set to `JWT` in the JWE protected header. |
| `invalid_request` | `MLE_SIGNATURE_VERIFICATION_FAILED` | 400 | The signature on the encrypted payload could not be verified against a registered signing key. |
| `invalid_request` | `MLE_INVALID_CLAIMS` | 400 | The signed payload claims are missing, malformed, or outside their validity window. |
| `invalid_request` | `MLE_ENCRYPTION_KEY_UNAVAILABLE` | 400 | No unambiguous active encryption key is registered for this team, so the response cannot be encrypted. |
| `invalid_request` | `MLE_SIGNING_REQUIRED` | 400 | Signed message level encryption is required for this team. Send a compact JWE body with `Content-Type: application/jose`. |
| `api_error` | `MLE_SIGNING_NOT_CONFIGURED` | 500 | Message level encryption (MLE) requests are temporarily unavailable. To ensure MLE try your request later, or fall back to a non-MLE request. |

## Performance Considerations

* MLE requests have increased latency due to encryption/decryption operations
* Consider implementing request timeouts appropriately
* Cache Method's public keys (respect the `Cache-Control` header)

## Key Lifecycle and Management

### Method's Key Status

Method's public keys have two possible statuses:

* **Active**: Current keys, each usable for its declared `use` — encryption keys for encrypting your requests, signing keys for verifying Method's responses
* **Deprecated**: Keys that are being phased out and will be disabled in 90 days

Always use keys with `status: "active"` when fetching Method's public keys. Deprecated keys remain functional for 90 days before being completely disabled.

Method's JWKS contains both encryption keys (`use: "enc"`, `alg: "RSA-OAEP-256"`) and signing keys (`use: "sig"`, `alg: "RS256"`). Filter on `use` as well as `status`: select an encryption key to encrypt your requests, and, on the signed path, a signing key to verify Method's response signatures. Every published key also carries numeric `iat`, `nbf`, and `exp` claims.

### Your Key Management

When you successfully register a key with Method, you'll receive a response like this:

```json theme={null}
{
  "success": true,
  "data": {
    "id": "team_jwk_12345",
    "type": "well_known",
    "jwk": "",
    "well_known_endpoint": "https://your-svc/.well-known/jwks.json",
    "status": "active",
    "contact": "",
    "created_at": "",
    "updated_at": ""
  },
  "message": null
}
```

### MLE Public Keys API

For complete CRUD operations on your MLE public keys, see the dedicated API documentation:

* **[Create MLE Public Key](/2026-03-30/reference/teams/mle/create)** - Register a new public key
* **[List MLE Public Keys](/2026-03-30/reference/teams/mle/list)** - Get your active registered keys
* **[Retrieve MLE Public Key](/2026-03-30/reference/teams/mle/retrieve)** - Get a specific key by ID
* **[Delete MLE Public Key](/2026-03-30/reference/teams/mle/delete)** - Delete (disable) a specific key

### Quick Key Deletion Example

You can delete your registered keys using the `id` returned when you created the key:

<CodeGroup>
  ```javascript Node.js theme={null}
  const response = await fetch('https://production.methodfi.com/teams/mle/public_keys/team_jwk_12345', {
    method: 'DELETE',
    headers: {
      'Authorization': 'Bearer sk_your_token'
    }
  });

  const result = await response.json();
  console.log('Deletion result:', result);
  ```

  ```python Python theme={null}
  response = requests.delete(
      'https://production.methodfi.com/teams/mle/public_keys/team_jwk_12345',
      headers={
          'Authorization': 'Bearer sk_your_token'
      }
  )

  print('Deletion result:', response.json())
  ```
</CodeGroup>

### Key Rotation Best Practices

* Method recommends rotating your keys every 90 days
* Always check for keys with `status: "active"` when fetching Method's keys
* Plan your key rotation to avoid service interruptions

### Webhook Notifications

You can subscribe to webhook events to be notified when Method's public keys change:

| Event Type | Description |
| - | - |
| `method_jwk.create` | Triggered when a new Method JWK (public key) is created |
| `method_jwk.update` | Triggered when a Method JWK is updated (deprecated or disabled) |

These webhooks fire for Method's signing keys as well as its encryption keys. They help you stay informed about Method's key lifecycle changes, allowing you to:

* Automatically fetch new active keys when they're created
* Update your cached keys when Method rotates or deprecates keys
* Implement proactive key management in your application

When a webhook is triggered, the event payload includes a `path` field pointing to the specific key that changed. You can use this path to retrieve the updated key information via the [Retrieve Method Public Key](/2026-03-30/reference/teams/mle/retrieve-method-key) endpoint.

Example webhook event:

```json theme={null}
{
  "id": "mthd_jwk_12",
  "type": "method_jwk.update",
  "path": "/auth/mthd_jwk_12",
  "event": "evt_knqJgxKUnqDVJ"
}
```

To subscribe to these events, create a webhook using the [Webhooks API](/2026-03-30/reference/webhooks/create) with the desired event type.

## Fallback Strategy

If MLE is temporarily unavailable (indicated by `MLE_DECRYPTION_FAILED` or `MLE_ENCRYPTION_FAILED` errors), you can fall back to standard non-encrypted requests by:

1. Remove the `Method-MLE: jwe` header
2. Send your payload directly (not wrapped in `encrypted`)
3. Process the plain response normally

This ensures your integration remains functional even during MLE service interruptions.


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