# Outbound API

The Outbound API handles file uploads to BCB STA (Sistema de Transferencia de Arquivos).

## Base URL

```
http://localhost:8080/api/v1
```

## Authentication

All Outbound API endpoints require API key authentication:

```bash
curl -H "Authorization: Bearer ${API_KEY}" http://localhost:8080/api/v1/health
```

Or using header:

```bash
curl -H "X-API-Key: your-api-key" http://localhost:8080/api/v1/health
```

## Endpoints

### Health Check

Check API and service health:

```http
GET /api/v1/health
```

Response (200 OK):

```json
{
  "status": "healthy",
  "sta_connection": "ok",
  "database": "ok",
  "uptime_seconds": 86400,
  "version": "1.0.0",
  "details": {
    "database_latency_ms": 5,
    "sta_latency_ms": 120,
    "last_sta_check": "2026-01-31T10:30:00Z",
    "pending_files": 3
  }
}
```

| Field | Description |
|-------|-------------|
| `status` | Overall status: `healthy` or `unhealthy` |
| `sta_connection` | STA WebService connectivity: `ok` or `error` |
| `database` | Database connectivity: `ok` or `error` |
| `uptime_seconds` | Service uptime in seconds |
| `version` | Connector version |

---

### Submit File

Submit a file for upload to BCB STA:

```http
POST /api/v1/files
Content-Type: application/json
Authorization: Bearer your-api-key

{
  "system": "CCS",
  "file_type": "ARQC001",
  "data": {
    "content": "SGVsbG8gV29ybGQgQkNCIEZpbGU=",
    "file_name": "CCS_20260131_001.txt"
  },
  "destination": {
    "ispb": "00000000"
  }
}
```

Request Body:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `system` | string | Yes | BCB system code (CCS, SPI, DICT, etc.) |
| `file_type` | string | Yes | BCB file type code |
| `data.content` | string | Yes | Base64-encoded file content |
| `data.file_name` | string | No | Optional filename (auto-generated if omitted) |
| `destination.ispb` | string | Yes* | Single destination ISPB |
| `destination.ispbs` | array | Yes* | Multiple destination ISPBs |

*Either `ispb` or `ispbs` is required.

Response (201 Created):

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "protocol": "2026013100001234",
  "status": "uploaded",
  "created_at": "2026-01-31T10:30:00Z",
  "file_name": "CCS_20260131_001.txt",
  "file_size": 12345,
  "checksum": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"
}
```

Response Fields:

| Field | Description |
|-------|-------------|
| `id` | Internal file ID (UUID) |
| `protocol` | BCB protocol number |
| `status` | Initial status (typically `uploaded`) |
| `created_at` | Submission timestamp (UTC) |
| `file_name` | Assigned filename |
| `file_size` | File size in bytes |
| `checksum` | SHA-256 checksum |

---

### Get File Status

Check the status of a submitted file:

```http
GET /api/v1/files/{id}/status
Authorization: Bearer your-api-key
```

Response (200 OK):

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "protocol": "2026013100001234",
  "file_name": "CCS_20260131_001.txt",
  "file_type": "ARQC001",
  "file_size": 12345,
  "status": "completed",
  "source_system": "CCS",
  "sta_state": {
    "sta_status": "ENTREGUE",
    "sta_message": "Arquivo entregue com sucesso",
    "sta_timestamp": "2026-01-31T10:30:15Z"
  },
  "created_at": "2026-01-31T10:30:00Z",
  "updated_at": "2026-01-31T10:30:15Z",
  "completed_at": "2026-01-31T10:30:15Z"
}
```

Status Values:

| Status | Description |
|--------|-------------|
| `pending` | Queued for processing |
| `uploading` | Upload in progress |
| `uploaded` | Sent to BCB, awaiting processing |
| `processing` | BCB is processing |
| `completed` | Successfully processed |
| `failed` | Processing failed |
| `retrying` | Scheduled for retry |

STA State Fields:

| Field | Description |
|-------|-------------|
| `sta_status` | BCB status code |
| `sta_message` | BCB status message |
| `sta_timestamp` | Last BCB update time |

---

## Validation

### System IDs

Valid BCB system codes:

| System | Name |
|--------|------|
| `CCS` | Customer Consultation System |
| `CIR` | Credit Information Registry |
| `CMP` | Means of Payment Control |
| `STR` | Reserve Transfer System |
| `SPI` | Instant Payment System (PIX) |
| `CAM` | Foreign Exchange System |
| `LDL` | Deferred Settlement |
| `DICT` | PIX Directory |

### Content Validation

| Check | Requirement |
|-------|-------------|
| Max file size | 50 MB |
| Content encoding | Valid base64 |
| Content length | Non-empty |
| System ID | Must be valid BCB system |
| ISPB | Must be 8 digits |

---

## Error Responses

All error responses follow this format:

```json
{
  "error": "error_code",
  "message": "Human-readable description"
}
```

### HTTP Status Codes

| Code | Description |
|------|-------------|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request (validation error) |
| 401 | Unauthorized (invalid API key) |
| 404 | Not Found |
| 429 | Rate Limited |
| 500 | Internal Server Error |
| 502 | Bad Gateway (STA error) |
| 503 | Service Unavailable |

### Error Codes

| Code | Description |
|------|-------------|
| `invalid_request` | Malformed request body |
| `validation_error` | Request validation failed |
| `invalid_content` | Invalid base64 content |
| `not_found` | File not found |
| `sta_error` | STA WebService error |
| `sta_auth_error` | STA authentication failed |
| `sta_rate_limit` | STA rate limit exceeded |
| `sta_connection_error` | Failed to connect to STA |
| `internal_error` | Internal server error |

---

## Code Examples

### cURL

```bash
# Health check
curl -H "Authorization: Bearer ${API_KEY}" \
  http://localhost:8080/api/v1/health

# Submit file
curl -X POST http://localhost:8080/api/v1/files \
  -H "Authorization: Bearer ${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "system": "CCS",
    "file_type": "ARQC001",
    "data": {
      "content": "'$(base64 -w0 file.txt)'",
      "file_name": "CCS_20260131_001.txt"
    },
    "destination": {
      "ispb": "00000000"
    }
  }'

# Check status
curl -H "Authorization: Bearer ${API_KEY}" \
  http://localhost:8080/api/v1/files/550e8400-.../status
```

### Python

```python
import requests
import base64
from typing import Optional

class STAConnectorClient:
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url.rstrip('/')
        self.headers = {
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json'
        }

    def health(self) -> dict:
        """Check service health."""
        response = requests.get(
            f'{self.base_url}/api/v1/health',
            headers=self.headers
        )
        response.raise_for_status()
        return response.json()

    def submit_file(
        self,
        system: str,
        file_type: str,
        content: bytes,
        ispb: str,
        file_name: Optional[str] = None
    ) -> dict:
        """Submit a file to BCB STA."""
        payload = {
            'system': system,
            'file_type': file_type,
            'data': {
                'content': base64.b64encode(content).decode('utf-8')
            },
            'destination': {
                'ispb': ispb
            }
        }

        if file_name:
            payload['data']['file_name'] = file_name

        response = requests.post(
            f'{self.base_url}/api/v1/files',
            headers=self.headers,
            json=payload
        )
        response.raise_for_status()
        return response.json()

    def get_status(self, file_id: str) -> dict:
        """Get file upload status."""
        response = requests.get(
            f'{self.base_url}/api/v1/files/{file_id}/status',
            headers=self.headers
        )
        response.raise_for_status()
        return response.json()

    def wait_for_completion(
        self,
        file_id: str,
        timeout: int = 300,
        poll_interval: int = 5
    ) -> dict:
        """Wait for file processing to complete."""
        import time
        start = time.time()

        while time.time() - start < timeout:
            status = self.get_status(file_id)

            if status['status'] in ('completed', 'failed'):
                return status

            time.sleep(poll_interval)

        raise TimeoutError(f'File {file_id} did not complete within {timeout}s')


# Usage example
if __name__ == '__main__':
    client = STAConnectorClient(
        base_url='http://localhost:8080',
        api_key='your-api-key'
    )

    # Check health
    health = client.health()
    print(f"Status: {health['status']}")

    # Submit file
    with open('CCS_file.txt', 'rb') as f:
        content = f.read()

    result = client.submit_file(
        system='CCS',
        file_type='ARQC001',
        content=content,
        ispb='00000000',
        file_name='CCS_20260131_001.txt'
    )

    print(f"Protocol: {result['protocol']}")
    print(f"File ID: {result['id']}")

    # Wait for completion
    final_status = client.wait_for_completion(result['id'])
    print(f"Final status: {final_status['status']}")
```

### Node.js

```javascript
const axios = require('axios');
const fs = require('fs');

class STAConnectorClient {
  constructor(baseUrl, apiKey) {
    this.client = axios.create({
      baseURL: baseUrl,
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      }
    });
  }

  async health() {
    const response = await this.client.get('/api/v1/health');
    return response.data;
  }

  async submitFile({ system, fileType, content, ispb, fileName }) {
    const base64Content = Buffer.isBuffer(content)
      ? content.toString('base64')
      : content;

    const payload = {
      system,
      file_type: fileType,
      data: {
        content: base64Content,
        ...(fileName && { file_name: fileName })
      },
      destination: {
        ispb
      }
    };

    const response = await this.client.post('/api/v1/files', payload);
    return response.data;
  }

  async getStatus(fileId) {
    const response = await this.client.get(`/api/v1/files/${fileId}/status`);
    return response.data;
  }

  async waitForCompletion(fileId, { timeout = 300000, pollInterval = 5000 } = {}) {
    const start = Date.now();

    while (Date.now() - start < timeout) {
      const status = await this.getStatus(fileId);

      if (['completed', 'failed'].includes(status.status)) {
        return status;
      }

      await new Promise(resolve => setTimeout(resolve, pollInterval));
    }

    throw new Error(`File ${fileId} did not complete within ${timeout}ms`);
  }
}

// Usage example
async function main() {
  const client = new STAConnectorClient(
    'http://localhost:8080',
    'your-api-key'
  );

  // Check health
  const health = await client.health();
  console.log('Status:', health.status);

  // Submit file
  const content = fs.readFileSync('CCS_file.txt');

  const result = await client.submitFile({
    system: 'CCS',
    fileType: 'ARQC001',
    content,
    ispb: '00000000',
    fileName: 'CCS_20260131_001.txt'
  });

  console.log('Protocol:', result.protocol);
  console.log('File ID:', result.id);

  // Wait for completion
  const finalStatus = await client.waitForCompletion(result.id);
  console.log('Final status:', finalStatus.status);
}

main().catch(console.error);
```

### Go

```go
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

type STAConnectorClient struct {
	BaseURL string
	APIKey  string
	Client  *http.Client
}

type SubmitRequest struct {
	System      string      `json:"system"`
	FileType    string      `json:"file_type"`
	Data        FileData    `json:"data"`
	Destination Destination `json:"destination"`
}

type FileData struct {
	Content  string `json:"content"`
	FileName string `json:"file_name,omitempty"`
}

type Destination struct {
	ISPB string `json:"ispb"`
}

type SubmitResponse struct {
	ID        string    `json:"id"`
	Protocol  string    `json:"protocol"`
	Status    string    `json:"status"`
	CreatedAt time.Time `json:"created_at"`
	FileName  string    `json:"file_name"`
	FileSize  int64     `json:"file_size"`
	Checksum  string    `json:"checksum"`
}

type StatusResponse struct {
	ID           string     `json:"id"`
	Protocol     string     `json:"protocol"`
	FileName     string     `json:"file_name"`
	FileType     string     `json:"file_type"`
	Status       string     `json:"status"`
	SourceSystem string     `json:"source_system"`
	CreatedAt    time.Time  `json:"created_at"`
	UpdatedAt    time.Time  `json:"updated_at"`
	CompletedAt  *time.Time `json:"completed_at,omitempty"`
}

func NewClient(baseURL, apiKey string) *STAConnectorClient {
	return &STAConnectorClient{
		BaseURL: baseURL,
		APIKey:  apiKey,
		Client:  &http.Client{Timeout: 30 * time.Second},
	}
}

func (c *STAConnectorClient) SubmitFile(system, fileType string, content []byte, ispb, fileName string) (*SubmitResponse, error) {
	reqBody := SubmitRequest{
		System:   system,
		FileType: fileType,
		Data: FileData{
			Content:  base64.StdEncoding.EncodeToString(content),
			FileName: fileName,
		},
		Destination: Destination{ISPB: ispb},
	}

	jsonBody, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}

	req, err := http.NewRequest("POST", c.BaseURL+"/api/v1/files", bytes.NewReader(jsonBody))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.APIKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.Client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("do request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusCreated {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, body)
	}

	var result SubmitResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}

	return &result, nil
}

func (c *STAConnectorClient) GetStatus(fileID string) (*StatusResponse, error) {
	req, err := http.NewRequest("GET", c.BaseURL+"/api/v1/files/"+fileID+"/status", nil)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.APIKey)

	resp, err := c.Client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("do request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, body)
	}

	var result StatusResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}

	return &result, nil
}

func main() {
	client := NewClient("http://localhost:8080", "your-api-key")

	// Read file content
	content, err := os.ReadFile("CCS_file.txt")
	if err != nil {
		panic(err)
	}

	// Submit file
	result, err := client.SubmitFile("CCS", "ARQC001", content, "00000000", "CCS_20260131_001.txt")
	if err != nil {
		panic(err)
	}

	fmt.Printf("Protocol: %s\n", result.Protocol)
	fmt.Printf("File ID: %s\n", result.ID)

	// Poll for completion
	for {
		status, err := client.GetStatus(result.ID)
		if err != nil {
			panic(err)
		}

		fmt.Printf("Status: %s\n", status.Status)

		if status.Status == "completed" || status.Status == "failed" {
			break
		}

		time.Sleep(5 * time.Second)
	}
}
```

---

## Rate Limiting

The connector enforces BCB rate limits:

- **Maximum 10** simultaneous uploads per institution
- **Maximum 120** queries per minute per institution

When rate limited, you'll receive a 429 response:

```json
{
  "error": "sta_rate_limit",
  "message": "STA rate limit exceeded"
}
```

Best practices:

1. Implement exponential backoff
2. Monitor rate limit metrics via Admin API
3. Queue uploads during high-traffic periods
4. Use the `Retry-After` header when provided

---

## Next Steps

- [Admin API](/api/admin) - Management and monitoring endpoints
- [Configuration](/guide/configuration) - Configuration options
- [Troubleshooting](/troubleshooting) - Common issues
