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.
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);

Step 2: Register Your Public Key with Method

You can register your public key using either a well-known endpoint (recommended) or direct registration.
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.
Host your public JWK at a well-known URL and register it with Method:
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);
Your well-known endpoint should return:
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "your-unique-key-id",
      "use": "enc",
      "alg": "RSA-OAEP-256",
      "n": "...",
      "e": "AQAB"
    }
  ]
}

Requirements for keys on .well-known end-point

  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 to the RFC
    4. JWK must have a field e and it must be a string that is valid e for a JWK in accordance to the RFC
    5. JWK can optionally have a field named alg but if it is provided the value must be RSA-OAEP-256
    6. JWK must have a field kid and it must be a string that is a valid id which will be passed as cid when making requests to Method

Option B: Direct Registration

Alternatively, register your public key directly:
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
  })
});

Step 3: Retrieve Method’s Public Key

Fetch Method’s public key for encrypting your requests:
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 key
const methodPublicKey = keys.find(k => k.status === 'active');
console.log('Method public key:', methodPublicKey);
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

Making Encrypted Requests

Step 1: Encrypt Your Request Payload

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');

Step 2: Send the 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'  // Required for MLE
  },
  body: JSON.stringify({
    encrypted: encryptedJwe
  })
});

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

Step 3: Decrypt the Response

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);

Complete Example

Here’s a complete example showing the full MLE flow:
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.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);
}

Error Handling

When using MLE, you may encounter these specific error codes:
Error Type CodeError Subtype CodeHTTP StatusDescription
api_errorMLE_DECRYPTION_FAILED500Message level encryption (MLE) requests are temporarily unavailable. To ensure MLE try your request later, or fall back to a non-MLE request.
api_errorMLE_ENCRYPTION_FAILED500Message level encryption (MLE) requests are temporarily unavailable. To ensure MLE try your request later, or fall back to a non-MLE request.
api_errorPAYLOAD_INVALID_JSON400The request body is not valid JSON.
invalid_requestMLE_INVALID_HEADER400When using message level encryption the header ‘Method-MLE’ must be set to ‘jwe’.
invalid_requestMLE_MISSING_ENCRYPTED_PAYLOAD400When using message level encryption the payload must contain field ‘encrypted’
invalid_requestMLE_UNSUPPORTED_KEY_MANAGEMENT_ALGORITHM400Key management algorithm provided is not supported. Must use ‘RSA-OAEP-256’
invalid_requestMLE_INVALID_ENCRYPTION_ALGORITHM400Unsupported or missing “enc” in protected header. Expected one of: ‘A256GCM’ or ‘A128GCM’.
invalid_requestMLE_MUST_INCLUDE_KID400Must include ‘KID’ in protected header.
invalid_requestMLE_MUST_INCLUDE_CID400Must include ‘CID’ in protected header.
invalid_requestMLE_INVALID_KID400The JWK KID is either disabled or does not exist.
invalid_requestMLE_INVALID_CID400The JWK CID is either disabled or does not exist.
invalid_requestMLE_INVALID_JWE_FORMAT400The MLE “encrypted” payload must be in the RFC compatible compact format.
invalid_requestWELL_KNOWN_ENDPOINT_ALREADY_EXISTS400An active well-known endpoint already exist. Must delete already existing well-known endpoint to post a new one.
invalid_requestJWK_KID_ALREADY_EXISTS400The specified JWK KID already exists.
invalid_requestJWK_ALREADY_DISABLED400The specified JWK is already disabled.
invalid_requestJWK_ALREADY_EXISTS400The specified JWK’s public content matches another key you have posted. Based on thumbprint from n , e , kty .

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 that should be used for encryption
  • 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.

Your Key Management

When you successfully register a key with Method, you’ll receive a response like this:
{
  "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:

Quick Key Deletion Example

You can delete your registered keys using the id returned when you created the key:
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);

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 TypeDescription
method_jwk.createTriggered when a new Method JWK (public key) is created
method_jwk.updateTriggered when a Method JWK is updated (deprecated or disabled)
These webhooks 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 endpoint. Example webhook event:
{
  "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 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.