Skip to main content
Idempotency ensures that repeated requests with the same idempotency key produce the same result, preventing duplicate charges and operations.

How It Works

Enable idempotency per request by setting the idempotencyKey field:
const result = await vault.charge({
  amount: 2500,
  currency: 'USD',
  paymentMethod: { type: 'card', token: 'pm_card_visa' },
  idempotencyKey: 'order-1001-charge-v1',
});

Behavior

Returns the original result from the idempotency store without making a new API call.
const first = await vault.charge({
  amount: 2500,
  currency: 'USD',
  paymentMethod: { type: 'card', token: 'pm_card_visa' },
  idempotencyKey: 'order-1001',
});

const replay = await vault.charge({
  amount: 2500,
  currency: 'USD',
  paymentMethod: { type: 'card', token: 'pm_card_visa' },
  idempotencyKey: 'order-1001',
});

console.log(first.id === replay.id); // true
The SDK compares request payloads using a cryptographic hash to detect conflicts efficiently.

Configuration

Configure idempotency behavior when creating the VaultClient:
import {
  MemoryIdempotencyStore,
  VaultClient,
} from '@vaultsaas/core';

const vault = new VaultClient({
  providers: { /* ... */ },
  routing: { /* ... */ },
  idempotency: {
    store: new MemoryIdempotencyStore(),
    ttlMs: 24 * 60 * 60 * 1000, // 24 hours (default)
  },
});

Configuration Options

OptionTypeDefaultDescription
storeIdempotencyStoreMemoryIdempotencyStoreStorage implementation
ttlMsnumber86400000 (24h)Time-to-live in milliseconds

Storage Implementations

MemoryIdempotencyStore

The default in-memory implementation:
import { MemoryIdempotencyStore } from '@vaultsaas/core';

const store = new MemoryIdempotencyStore();
Not suitable for production multi-instance deployments. Each instance maintains its own in-memory store, so requests to different instances won’t share idempotency records.

Custom Store Implementation

Implement the IdempotencyStore interface for distributed storage:
IdempotencyStore Interface
export interface IdempotencyStore<T = unknown> {
  get(
    key: string,
  ): Promise<IdempotencyRecord<T> | null> | IdempotencyRecord<T> | null;
  set(record: IdempotencyRecord<T>): Promise<void> | void;
  delete(key: string): Promise<void> | void;
  clearExpired(now?: number): Promise<void> | void;
}

export interface IdempotencyRecord<T = unknown> {
  key: string;
  payloadHash: string;
  result: T;
  expiresAt: number;
}
import { createClient } from 'redis';
import type { IdempotencyStore, IdempotencyRecord } from '@vaultsaas/core';

export class RedisIdempotencyStore implements IdempotencyStore {
  constructor(private client: ReturnType<typeof createClient>) {}

  async get(key: string): Promise<IdempotencyRecord | null> {
    const data = await this.client.get(`idempotency:${key}`);
    if (!data) return null;
    return JSON.parse(data);
  }

  async set(record: IdempotencyRecord): Promise<void> {
    const ttl = Math.ceil((record.expiresAt - Date.now()) / 1000);
    await this.client.setEx(
      `idempotency:${record.key}`,
      ttl,
      JSON.stringify(record)
    );
  }

  async delete(key: string): Promise<void> {
    await this.client.del(`idempotency:${key}`);
  }

  async clearExpired(): Promise<void> {
    // Redis handles expiration automatically
  }
}

Implementation Details

Payload Hashing

The SDK creates a SHA-256 hash of the operation and request payload:
src/client/vault-client.ts:604
const payloadHash = hashIdempotencyPayload({
  operation,
  request,
});

Conflict Detection

src/client/vault-client.ts:610
if (existingRecord) {
  if (existingRecord.payloadHash !== payloadHash) {
    throw new VaultIdempotencyConflictError(
      'Idempotency key was reused with a different payload.',
      {
        operation,
        key,
      },
    );
  }

  return existingRecord.result as TResult;
}

Record Storage

src/client/vault-client.ts:624
const result = await execute();

await this.idempotencyStore.set({
  key,
  payloadHash,
  result,
  expiresAt: Date.now() + this.idempotencyTtlMs,
});

return result;

Complete Example

import {
  MemoryIdempotencyStore,
  StripeAdapter,
  VaultClient,
  VaultIdempotencyConflictError,
} from '@vaultsaas/core';

function mustEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing env var: ${name}`);
  return value;
}

const vault = new VaultClient({
  providers: {
    stripe: {
      adapter: StripeAdapter,
      config: {
        apiKey: mustEnv('STRIPE_API_KEY'),
      },
    },
  },
  routing: {
    rules: [{ match: { default: true }, provider: 'stripe' }],
  },
  idempotency: {
    store: new MemoryIdempotencyStore(),
    ttlMs: 24 * 60 * 60 * 1000,
  },
});

const first = await vault.charge({
  amount: 2500,
  currency: 'USD',
  paymentMethod: {
    type: 'card',
    number: '4242424242424242',
    expMonth: 12,
    expYear: 2030,
    cvc: '123',
  },
  idempotencyKey: 'order-1001-charge-v1',
});

const replay = await vault.charge({
  amount: 2500,
  currency: 'USD',
  paymentMethod: {
    type: 'card',
    number: '4242424242424242',
    expMonth: 12,
    expYear: 2030,
    cvc: '123',
  },
  idempotencyKey: 'order-1001-charge-v1',
});

console.log(first.id === replay.id); // true

try {
  await vault.charge({
    amount: 3000,
    currency: 'USD',
    paymentMethod: {
      type: 'card',
      number: '4242424242424242',
      expMonth: 12,
      expYear: 2030,
      cvc: '123',
    },
    idempotencyKey: 'order-1001-charge-v1',
  });
} catch (error) {
  if (error instanceof VaultIdempotencyConflictError) {
    console.error(error.code, error.suggestion);
  }
}

Best Practices

Generate unique keys per operationInclude the order ID, operation type, and version:
const idempotencyKey = `order-${orderId}-charge-v${version}`;
Use distributed storage in productionImplement Redis or database-backed storage for multi-instance deployments.
Set appropriate TTLBalance storage costs with retry windows:
  • Short-lived operations: 1-6 hours
  • Critical operations: 24-72 hours
Don’t reuse keys across different operationsUse separate keys for charge vs. refund, even for the same order.

Next Steps

Error Handling

Handle idempotency conflicts

Architecture

Learn about the idempotency store

Build docs developers (and LLMs) love