# Phoenix Outbound API

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

## Base URL

```
http://localhost:4000/api/v1
```

## Authentication

All Outbound API endpoints require API key authentication:

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

Or using header:

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

## Endpoints

### Health Check

Check API and service health:

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

Response (200 OK):

```json
{
  "data": {
    "status": "healthy",
    "sta_connection": "ok",
    "database": "ok",
    "uptime_seconds": 86400,
    "version": "1.0.0",
    "node": "sta_connector@127.0.0.1",
    "details": {
      "database_latency_ms": 5,
      "sta_latency_ms": 120,
      "last_sta_check": "2026-01-31T10:30:00Z",
      "pending_files": 3,
      "otp_applications": ["sta_connector", "phoenix", "ecto"]
    }
  }
}
```

| Field | Description |
|-------|-------------|
| `data.status` | Overall status: `healthy` or `unhealthy` |
| `data.sta_connection` | STA WebService connectivity: `ok` or `error` |
| `data.database` | Database connectivity: `ok` or `error` |
| `data.uptime_seconds` | Service uptime in seconds |
| `data.version` | Connector version |
| `data.node` | Erlang/Elixir node name |

---

### Submit File

Submit a file for upload to BCB STA:

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

{
  "file": {
    "system": "CCS",
    "file_type": "ARQC001",
    "content": "SGVsbG8gV29ybGQgQkNCIEZpbGU=",
    "file_name": "CCS_20260131_001.txt",
    "destination_ispb": "00000000"
  }
}
```

Request Body:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `file.system` | string | Yes | BCB system code (CCS, SPI, DICT, etc.) |
| `file.file_type` | string | Yes | BCB file type code |
| `file.content` | string | Yes | Base64-encoded file content |
| `file.file_name` | string | No | Optional filename (auto-generated if omitted) |
| `file.destination_ispb` | string | Yes | Destination ISPB (8 digits) |
| `file.destination_ispbs` | array | No | Multiple destination ISPBs (alternative) |

Response (201 Created):

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

Response Fields:

| Field | Description |
|-------|-------------|
| `data.id` | Internal file ID (UUID) |
| `data.protocol` | BCB protocol number |
| `data.status` | Initial status (typically `uploaded`) |
| `data.inserted_at` | Submission timestamp (UTC) |
| `data.updated_at` | Last update timestamp (UTC) |
| `data.file_name` | Assigned filename |
| `data.file_size` | File size in bytes |
| `data.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
{
  "data": {
    "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"
    },
    "inserted_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 |

---

## Phoenix Channel Events

Subscribe to real-time file status updates via Phoenix Channels.

### Connecting to the Socket

```javascript
import { Socket } from "phoenix";

const socket = new Socket("ws://localhost:4000/socket", {
  params: { token: "your-api-key" }
});

socket.connect();
```

### Joining the Files Channel

```javascript
// Join channel for a specific file
const channel = socket.channel(`files:${fileId}`, {});

channel.join()
  .receive("ok", resp => console.log("Joined successfully", resp))
  .receive("error", resp => console.log("Unable to join", resp));

// Listen for status updates
channel.on("status_changed", payload => {
  console.log("File status changed:", payload);
  // payload: { id, status, sta_state, updated_at }
});

channel.on("upload_progress", payload => {
  console.log("Upload progress:", payload);
  // payload: { id, percent_complete, bytes_uploaded, total_bytes }
});

channel.on("completed", payload => {
  console.log("File completed:", payload);
  // payload: { id, protocol, status, completed_at }
});

channel.on("failed", payload => {
  console.log("File failed:", payload);
  // payload: { id, error, error_message, failed_at }
});
```

### Joining the Institution Channel

```javascript
// Join channel for all files from an institution
const institutionChannel = socket.channel(`institution:${ispb}`, {});

institutionChannel.join()
  .receive("ok", resp => console.log("Joined institution channel"))
  .receive("error", resp => console.log("Unable to join", resp));

// Listen for all file events for this institution
institutionChannel.on("file_submitted", payload => {
  console.log("New file submitted:", payload);
});

institutionChannel.on("file_status_changed", payload => {
  console.log("File status changed:", payload);
});
```

### Channel Events Reference

| Event | Channel | Description |
|-------|---------|-------------|
| `status_changed` | `files:{id}` | File status has changed |
| `upload_progress` | `files:{id}` | Upload progress update (percent) |
| `completed` | `files:{id}` | File processing completed successfully |
| `failed` | `files:{id}` | File processing failed |
| `file_submitted` | `institution:{ispb}` | New file submitted for institution |
| `file_status_changed` | `institution:{ispb}` | Any file status change for institution |

---

## 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 Phoenix JSON:API format:

```json
{
  "errors": [
    {
      "status": "422",
      "code": "validation_error",
      "title": "Validation Error",
      "detail": "content can't be blank",
      "source": {
        "pointer": "/file/content"
      }
    }
  ]
}
```

### HTTP Status Codes

| Code | Description |
|------|-------------|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request (validation error) |
| 401 | Unauthorized (invalid API key) |
| 404 | Not Found |
| 422 | Unprocessable Entity (validation failed) |
| 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:4000/api/v1/health

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

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

### Elixir

```elixir
defmodule STAConnectorClient do
  @moduledoc """
  Client for the Phoenix STA Connector Outbound API.
  """

  @base_url "http://localhost:4000/api/v1"

  def health(api_key) do
    headers = [{"Authorization", "Bearer #{api_key}"}]

    case HTTPoison.get("#{@base_url}/health", headers) do
      {:ok, %{status_code: 200, body: body}} ->
        {:ok, Jason.decode!(body)}

      {:ok, %{status_code: status, body: body}} ->
        {:error, %{status: status, body: Jason.decode!(body)}}

      {:error, reason} ->
        {:error, reason}
    end
  end

  def submit_file(api_key, params) do
    headers = [
      {"Authorization", "Bearer #{api_key}"},
      {"Content-Type", "application/json"}
    ]

    body = Jason.encode!(%{
      file: %{
        system: params.system,
        file_type: params.file_type,
        content: Base.encode64(params.content),
        file_name: params[:file_name],
        destination_ispb: params.destination_ispb
      }
    })

    case HTTPoison.post("#{@base_url}/files", body, headers) do
      {:ok, %{status_code: 201, body: body}} ->
        {:ok, Jason.decode!(body)}

      {:ok, %{status_code: status, body: body}} ->
        {:error, %{status: status, body: Jason.decode!(body)}}

      {:error, reason} ->
        {:error, reason}
    end
  end

  def get_status(api_key, file_id) do
    headers = [{"Authorization", "Bearer #{api_key}"}]

    case HTTPoison.get("#{@base_url}/files/#{file_id}/status", headers) do
      {:ok, %{status_code: 200, body: body}} ->
        {:ok, Jason.decode!(body)}

      {:ok, %{status_code: status, body: body}} ->
        {:error, %{status: status, body: Jason.decode!(body)}}

      {:error, reason} ->
        {:error, reason}
    end
  end

  def wait_for_completion(api_key, file_id, opts \\ []) do
    timeout = Keyword.get(opts, :timeout, 300_000)
    poll_interval = Keyword.get(opts, :poll_interval, 5_000)

    start_time = System.monotonic_time(:millisecond)

    Stream.repeatedly(fn ->
      Process.sleep(poll_interval)
      get_status(api_key, file_id)
    end)
    |> Stream.take_while(fn
      {:ok, %{"data" => %{"status" => status}}} when status in ["completed", "failed"] ->
        false

      _ ->
        System.monotonic_time(:millisecond) - start_time < timeout
    end)
    |> Enum.to_list()
    |> List.last()
    |> case do
      {:ok, result} -> {:ok, result}
      nil -> get_status(api_key, file_id)
      error -> error
    end
  end
end

# Usage example
api_key = "your-api-key"

# Check health
{:ok, health} = STAConnectorClient.health(api_key)
IO.inspect(health["data"]["status"], label: "Status")

# Submit file
content = File.read!("CCS_file.txt")

{:ok, result} = STAConnectorClient.submit_file(api_key, %{
  system: "CCS",
  file_type: "ARQC001",
  content: content,
  destination_ispb: "00000000",
  file_name: "CCS_20260131_001.txt"
})

IO.puts("Protocol: #{result["data"]["protocol"]}")
IO.puts("File ID: #{result["data"]["id"]}")

# Wait for completion
{:ok, final_status} = STAConnectorClient.wait_for_completion(api_key, result["data"]["id"])
IO.puts("Final status: #{final_status["data"]["status"]}")
```

### Elixir with Phoenix Channels

```elixir
defmodule STAConnectorSocket do
  @moduledoc """
  Phoenix Channel client for real-time file status updates.
  """

  use Phoenix.ChannelClient

  def start_link(api_key) do
    Phoenix.ChannelClient.start_link(
      __MODULE__,
      url: "ws://localhost:4000/socket/websocket",
      params: %{token: api_key}
    )
  end

  def join_file_channel(socket, file_id) do
    Phoenix.ChannelClient.join(socket, "files:#{file_id}", %{})
  end

  def join_institution_channel(socket, ispb) do
    Phoenix.ChannelClient.join(socket, "institution:#{ispb}", %{})
  end

  # Callbacks
  def handle_message("status_changed", payload, state) do
    IO.inspect(payload, label: "Status changed")
    {:noreply, state}
  end

  def handle_message("completed", payload, state) do
    IO.inspect(payload, label: "File completed")
    {:noreply, state}
  end

  def handle_message("failed", payload, state) do
    IO.inspect(payload, label: "File failed")
    {:noreply, state}
  end
end
```

### Python

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

class STAConnectorClient:
    def __init__(self, base_url: str = "http://localhost:4000", api_key: str = None):
        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,
        destination_ispb: str,
        file_name: Optional[str] = None
    ) -> dict:
        """Submit a file to BCB STA."""
        payload = {
            'file': {
                'system': system,
                'file_type': file_type,
                'content': base64.b64encode(content).decode('utf-8'),
                'destination_ispb': destination_ispb
            }
        }

        if file_name:
            payload['file']['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['data']['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:4000',
        api_key='your-api-key'
    )

    # Check health
    health = client.health()
    print(f"Status: {health['data']['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,
        destination_ispb='00000000',
        file_name='CCS_20260131_001.txt'
    )

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

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

### Node.js

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

class STAConnectorClient {
  constructor(baseUrl = 'http://localhost:4000', 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, destinationIspb, fileName }) {
    const base64Content = Buffer.isBuffer(content)
      ? content.toString('base64')
      : content;

    const payload = {
      file: {
        system,
        file_type: fileType,
        content: base64Content,
        destination_ispb: destinationIspb,
        ...(fileName && { file_name: fileName })
      }
    };

    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.data.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:4000',
    'your-api-key'
  );

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

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

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

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

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

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

### Node.js with Phoenix Channels

```javascript
const { Socket } = require('phoenix-channels');

// Connect to Phoenix socket
const socket = new Socket('ws://localhost:4000/socket', {
  params: { token: 'your-api-key' }
});

socket.connect();

// Join file-specific channel
const fileId = '550e8400-e29b-41d4-a716-446655440000';
const channel = socket.channel(`files:${fileId}`, {});

channel.join()
  .receive('ok', resp => {
    console.log('Joined file channel successfully', resp);
  })
  .receive('error', resp => {
    console.log('Unable to join', resp);
  });

// Listen for events
channel.on('status_changed', payload => {
  console.log('Status changed:', payload);
});

channel.on('upload_progress', payload => {
  console.log(`Upload progress: ${payload.percent_complete}%`);
});

channel.on('completed', payload => {
  console.log('File completed:', payload);
  channel.leave();
});

channel.on('failed', payload => {
  console.error('File failed:', payload);
  channel.leave();
});
```

---

## 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
{
  "errors": [
    {
      "status": "429",
      "code": "sta_rate_limit",
      "title": "Rate Limit Exceeded",
      "detail": "STA rate limit exceeded. Please retry after 60 seconds."
    }
  ]
}
```

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
5. Subscribe to Phoenix Channels for real-time status instead of polling

---

## Next Steps

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