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

# Add payments to a server

> Authenticated Express and Lambda payment handlers with validation, idempotency, and reconciliation.

Use these handlers after [installing the SDK](/docs/sdk/installation). They are integration examples:
provide your own application authentication and authorization, and configure the recipient allowlist
before exposing a payment route. For a runnable terminal example, use [First payment](/docs/quickstart/setup).

## Framework Integration

### Next.js

```typescript theme={null}
// lib/conto.ts
import { Conto } from '@conto_finance/sdk';

export const conto = new Conto({
  apiKey: process.env.CONTO_API_KEY!,
});
```

### Express

Do not pass an untrusted request body directly to `payments.pay()`. Put authentication, local
limits, a recipient allowlist, and a caller-supplied idempotency key in front of the SDK. This
shared server helper is used by both examples below:

```typescript theme={null}
// safe-payment.ts
import { Conto, ContoError, type PaymentRequestInput } from '@conto_finance/sdk';

const conto = new Conto({
  apiKey: process.env.CONTO_API_KEY!,
  timeout: 15_000,
});

const maxAmount = Number(process.env.MAX_PAYMENT_AMOUNT ?? '50');
if (!Number.isFinite(maxAmount) || maxAmount <= 0) {
  throw new Error('MAX_PAYMENT_AMOUNT must be a positive number');
}
const allowedRecipients = new Set(
  (process.env.PAYMENT_RECIPIENT_ALLOWLIST ?? '')
    .split(',')
    .map((address) => address.trim().toLowerCase())
    .filter(Boolean)
);

type PaymentCommand = Pick<PaymentRequestInput, 'amount' | 'recipientAddress' | 'purpose'>;

export function parsePayment(value: unknown): PaymentCommand {
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
    throw new Error('Body must be an object');
  }
  const body = value as Record<string, unknown>;
  const address = typeof body.recipientAddress === 'string' ? body.recipientAddress : '';
  const purpose = typeof body.purpose === 'string' ? body.purpose.trim() : '';
  if (
    typeof body.amount !== 'number' ||
    !Number.isFinite(body.amount) ||
    body.amount <= 0 ||
    body.amount > maxAmount ||
    !/^0x[0-9a-fA-F]{40}$/.test(address) ||
    !allowedRecipients.has(address.toLowerCase()) ||
    purpose.length < 1 ||
    purpose.length > 200
  ) {
    throw new Error('Invalid amount, recipient, or purpose');
  }
  return { amount: body.amount, recipientAddress: address, purpose };
}

export async function submitPayment(input: PaymentCommand, idempotencyKey: string) {
  const request = await conto.payments.request({ ...input, idempotencyKey });

  if (request.status !== 'APPROVED') {
    const statusCode =
      request.status === 'DENIED' ? 403 : request.status === 'REQUIRES_APPROVAL' ? 202 : 409;
    return {
      statusCode,
      body: { requestId: request.requestId, status: request.status, reasons: request.reasons },
    };
  }

  try {
    const result = await conto.payments.execute(request.requestId);
    return {
      statusCode: result.status === 'processing' ? 202 : 200,
      body: result,
    };
  } catch (error) {
    const deterministicClientError =
      error instanceof ContoError &&
      error.status >= 400 &&
      error.status < 500 &&
      ![408, 429].includes(error.status);
    if (deterministicClientError) throw error;

    // Execution may have succeeded before a timeout, connection drop, 429, or 5xx.
    const state = await conto.payments.status(request.requestId).catch(() => null);
    const transaction = state?.transaction;
    return {
      statusCode:
        transaction?.status === 'failed' ? 409 : transaction?.status === 'completed' ? 200 : 202,
      body: {
        requestId: request.requestId,
        status: transaction?.status ?? 'UNKNOWN',
        transaction,
        message: 'Execution result was reconciled; do not blindly retry execute.',
      },
    };
  }
}

export function mapPaymentError(error: unknown) {
  if (error instanceof ContoError) {
    if (error.code === 'ACTION_REQUIRED') {
      return {
        statusCode: 402,
        body: {
          error: error.code,
          requestId: error.requestId,
          actionUrl: error.actionUrl,
        },
      };
    }
    let statusCode = 502;
    if (error.code === 'INVALID_INPUT') statusCode = 400;
    if (['IDEMPOTENCY_CONFLICT', 'INSUFFICIENT_BALANCE'].includes(error.code)) statusCode = 409;
    if (error.code === 'RATE_LIMITED') statusCode = 503;
    return { statusCode, body: { error: error.code } };
  }
  return { statusCode: 502, body: { error: 'PAYMENT_SERVICE_ERROR' } };
}
```

Set `PAYMENT_RECIPIENT_ALLOWLIST` to comma-separated EVM addresses and `MAX_PAYMENT_AMOUNT` to your
application-level cap. Then wire the helper to an authenticated and authorized Express route. The
middleware must verify a user or service identity, enforce your application's payment-operator
permission, and set `res.locals.subject`; replace the sample import with your implementation.

```typescript theme={null}
// app.ts
import { createHash } from 'node:crypto';
import express from 'express';
import { requirePaymentOperator } from './auth';
import { mapPaymentError, parsePayment, submitPayment } from './safe-payment';

const app = express();
app.use(express.json({ limit: '16kb' }));

app.post('/payments/submit', requirePaymentOperator, async (req, res) => {
  const callerKey = req.get('Idempotency-Key');
  if (!callerKey || !/^[A-Za-z0-9:_-]{8,64}$/.test(callerKey)) {
    return res.status(400).json({ error: 'A stable Idempotency-Key is required' });
  }

  let payment: ReturnType<typeof parsePayment>;
  try {
    payment = parsePayment(req.body);
  } catch {
    return res.status(400).json({ error: 'Invalid payment request' });
  }

  const subject = res.locals.subject;
  if (typeof subject !== 'string' || !subject) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  const idempotencyKey = createHash('sha256').update(`${subject}:${callerKey}`).digest('hex');

  try {
    const response = await submitPayment(payment, idempotencyKey);
    return res.status(response.statusCode).json(response.body);
  } catch (error) {
    const response = mapPaymentError(error);
    return res.status(response.statusCode).json(response.body);
  }
});
```

### Serverless (AWS Lambda)

Configure this function behind an API Gateway JWT authorizer and require a `payments:create` scope
on the route. It also checks that scope in the handler, rejects requests without a verified
subject, validates and caps the body, requires idempotency, and uses the same recipient allowlist
and error mapping as the Express route.

```typescript theme={null}
import { createHash } from 'node:crypto';
import { mapPaymentError, parsePayment, submitPayment } from './safe-payment';

export const handler = async (event: any) => {
  const claims = event.requestContext?.authorizer?.jwt?.claims;
  const subject = claims?.sub;
  if (typeof subject !== 'string' || !subject) {
    return { statusCode: 401, body: JSON.stringify({ error: 'Unauthorized' }) };
  }

  const scopes = typeof claims.scope === 'string' ? claims.scope.split(/\s+/) : [];
  if (!scopes.includes('payments:create')) {
    return { statusCode: 403, body: JSON.stringify({ error: 'Forbidden' }) };
  }

  const callerKey = event.headers?.['idempotency-key'] ?? event.headers?.['Idempotency-Key'];
  if (typeof callerKey !== 'string' || !/^[A-Za-z0-9:_-]{8,64}$/.test(callerKey)) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error: 'A stable Idempotency-Key is required' }),
    };
  }

  const rawBody = event.isBase64Encoded
    ? Buffer.from(event.body ?? '', 'base64').toString('utf8')
    : (event.body ?? '');
  if (rawBody.length > 16_384) {
    return { statusCode: 413, body: JSON.stringify({ error: 'Request body too large' }) };
  }

  let payment: ReturnType<typeof parsePayment>;
  try {
    payment = parsePayment(JSON.parse(rawBody));
  } catch {
    return { statusCode: 400, body: JSON.stringify({ error: 'Invalid payment request' }) };
  }

  const idempotencyKey = createHash('sha256').update(`${subject}:${callerKey}`).digest('hex');
  try {
    const response = await submitPayment(payment, idempotencyKey);
    return { statusCode: response.statusCode, body: JSON.stringify(response.body) };
  } catch (error) {
    const response = mapPaymentError(error);
    return { statusCode: response.statusCode, body: JSON.stringify(response.body) };
  }
};
```

Continue with [payment recovery](/docs/guides/payment-lifecycle) and [webhook delivery](/docs/guides/webhooks).
