# Asaas API Integration Standard

> **Scope:** universal
> **Layer:** 2 (on keyword)
> **Keywords:** asaas, payment, brazilian, pix, boleto, billing
> **Load When:** asaas or brazilian payment keywords detected

**Verified against:** Asaas REST API v3 (no official Node SDK — direct REST). Last-verified: 2026-05-20.

---

Brazilian payment gateway integration for PIX, Boleto, and credit card processing.

---

## Overview

Asaas is a Brazilian payment service provider offering:
- PIX instant payments
- Boleto bancário
- Credit card processing
- Subscription billing
- Split payments

**Stack:** .NET Backend

---

## Core Principles

1. **Webhook-First**: All payment state changes via webhooks
2. **Idempotency**: Use idempotency keys for all POST requests
3. **Security**: Validate webhook signatures, never store full card data
4. **Brazil-Specific**: Handle CPF/CNPJ validation, Brazilian tax rules

---

## API Configuration

### Environment Variables

```bash
# .env.local
ASAAS_API_KEY=your_api_key_here
ASAAS_WEBHOOK_SECRET=your_webhook_secret
ASAAS_ENVIRONMENT=sandbox # or production
```

### REST Client Setup

Asaas has **no official Node SDK** — integrate against the documented REST API
directly. The API key goes in the `access_token` header; the base URL switches
between sandbox and production.

```typescript
// lib/asaas/client.ts
const ASAAS_BASE_URL =
  process.env.ASAAS_ENVIRONMENT === 'production'
    ? 'https://api.asaas.com/v3'
    : 'https://api-sandbox.asaas.com/v3';

export async function asaasFetch<T>(
  path: string,
  init?: RequestInit,
): Promise<T> {
  const res = await fetch(`${ASAAS_BASE_URL}${path}`, {
    ...init,
    headers: {
      'Content-Type': 'application/json',
      access_token: process.env.ASAAS_API_KEY!,
      ...init?.headers,
    },
  });
  if (!res.ok) {
    throw new Error(`Asaas ${res.status}: ${await res.text()}`);
  }
  return res.json() as Promise<T>;
}
```

---

## Common Operations

### Create PIX Payment

```typescript
// app/api/payments/pix/route.ts
import { asaasFetch } from '@/lib/asaas/client';

export async function POST(req: Request) {
  const { amount, customerId, description } = await req.json();

  // POST /v3/payments — billingType PIX
  const payment = await asaasFetch<{ id: string }>('/payments', {
    method: 'POST',
    body: JSON.stringify({
      customer: customerId,
      billingType: 'PIX',
      value: amount,
      dueDate: new Date().toISOString().split('T')[0],
      description,
    }),
  });

  // GET /v3/payments/{id}/pixQrCode — PIX copy-paste payload + QR image
  const pix = await asaasFetch<{ payload: string; encodedImage: string }>(
    `/payments/${payment.id}/pixQrCode`,
  );

  return Response.json({
    paymentId: payment.id,
    pixCode: pix.payload,
    qrCode: pix.encodedImage,
  });
}
```

### Webhook Handler

Asaas does not HMAC-sign webhooks. Instead, every webhook request carries the
`authToken` you configured for the webhook in the `asaas-access-token` header —
compare it against your stored secret. The `authToken` is mandatory: a `POST
/v3/webhooks` without one makes Asaas auto-generate a high-security token
(32–255 chars, shown once in the response).

```typescript
// app/api/webhooks/asaas/route.ts
import { headers } from 'next/headers';

export async function POST(req: Request) {
  const body = await req.text();

  // Asaas sends the webhook's authToken in the asaas-access-token header
  const token = headers().get('asaas-access-token');
  if (token !== process.env.ASAAS_WEBHOOK_SECRET) {
    return new Response('Invalid token', { status: 401 });
  }

  const event = JSON.parse(body);

  // Handle payment events
  switch (event.event) {
    case 'PAYMENT_RECEIVED':
      await handlePaymentReceived(event.payment);
      break;
    case 'PAYMENT_CONFIRMED':
      await handlePaymentConfirmed(event.payment);
      break;
  }

  return new Response('OK');
}
```

---

## Database Schema (PostgreSQL / Neon)

```sql
-- Store payment records
create table payments (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id),
  asaas_payment_id text unique not null,
  amount decimal(10,2) not null,
  status text not null, -- pending, confirmed, received
  billing_type text not null, -- PIX, BOLETO, CREDIT_CARD
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- RLS policies
alter table payments enable row level security;

create policy "Users read own payments"
  on payments for select
  using (auth.user_id() = user_id);
```

---

## Best Practices

### CPF/CNPJ Validation

```typescript
export function validateCPF(cpf: string): boolean {
  cpf = cpf.replace(/[^\d]/g, '');
  if (cpf.length !== 11) return false;

  // Validation logic...
  return true;
}

export function validateCNPJ(cnpj: string): boolean {
  cnpj = cnpj.replace(/[^\d]/g, '');
  if (cnpj.length !== 14) return false;

  // Validation logic...
  return true;
}
```

### Idempotency Keys

Pass a stable idempotency key in the request header so a retried POST does not
create a duplicate charge:

```typescript
await asaasFetch('/payments', {
  method: 'POST',
  headers: { 'Idempotency-Key': `payment-${userId}-${requestId}` },
  body: JSON.stringify({ /* ... payment data ... */ }),
});
```

---

## Error Handling

Asaas returns failures as HTTP 400 with a JSON body shaped `{ errors: [{ code,
description }] }`. `asaasFetch` throws on non-OK responses — catch and inspect
the `errors[]` array:

```typescript
type AsaasError = { errors: { code: string; description: string }[] };

try {
  await asaasFetch('/payments', { method: 'POST', body: JSON.stringify(data) });
} catch (error) {
  // asaasFetch throws `Asaas <status>: <raw body>` — parse the body
  const body = (error as Error).message.replace(/^Asaas \d+: /, '');
  let parsed: AsaasError | null = null;
  try { parsed = JSON.parse(body) as AsaasError; } catch { /* non-JSON */ }

  const code = parsed?.errors?.[0]?.code;
  if (code === 'invalid_cpfCnpj') {
    return Response.json({ error: 'CPF/CNPJ inválido' }, { status: 400 });
  }

  console.error('Asaas API error:', body);
  return Response.json({ error: 'Erro no processamento' }, { status: 500 });
}
```

---

## References

- [Asaas API Documentation](https://docs.asaas.com/)
- [Asaas Webhooks](https://docs.asaas.com/docs/about-webhooks)
- PIX Specification: Brazilian Central Bank

> Asaas publishes **no official Node SDK** — integrate against the REST API
> directly. Community wrappers exist but are unofficial and vary in maintenance.

---

*MORPH-SPEC by Polymorphism Tech*
