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

# Authentication

> EdDSA Proof-of-Possession (PoP) authentication for the Banking API

# Authentication

The Banking API uses **EdDSA Proof-of-Possession (PoP)** authentication. This cryptographic method ensures that requests are signed by the legitimate owner of the service account's private key and prevents replay attacks.

## Overview

Unlike traditional API key or OAuth authentication, PoP authentication requires you to cryptographically sign each request. This provides:

* **Non-repudiation** - Requests can be proven to originate from your service account
* **Replay protection** - Timestamps prevent reuse of captured requests

## Prerequisites

Before making API requests, you need:

1. **Service Account** - Created in your Conta Digital dashboard
2. **Ed25519 Key Pair** - Generate a public/private key pair
3. **Registered Public Key** - Upload your public key to your service account

## Required Headers

Every authenticated request must include these 4 headers:

| Header            | Description                    | Example                                |
| ----------------- | ------------------------------ | -------------------------------------- |
| `x-access-id`     | Your service account UUID      | `550e8400-e29b-41d4-a716-446655440000` |
| `X-PoP-Signature` | EdDSA signature (base64)       | `MEUCIQDx...`                          |
| `X-PoP-Challenge` | Unix timestamp in milliseconds | `1705423200000`                        |
| `X-PoP-Format`    | Authentication type identifier | `service-account`                      |

## Signature Process

The signature is created by signing a specific message format with your Ed25519 private key.

### Step 1: Construct the Message

Concatenate the following values separated by colons (`:`):

```
{uri}:{method}:{body}:{timestamp}
```

| Component   | Description                                                                    |
| ----------- | ------------------------------------------------------------------------------ |
| `uri`       | Full request path including query string (e.g., `/v1/account?include=balance`) |
| `method`    | HTTP method in uppercase (e.g., `GET`, `POST`)                                 |
| `body`      | Request body as string, or empty string for GET requests                       |
| `timestamp` | Same value as `X-PoP-Challenge` header                                         |

### Step 2: Sign the Message

Sign the message using your Ed25519 private key and encode the signature in base64.

### Step 3: Set Headers

Include the signature and all required headers in your request.

## Code Examples

### Node.js

```javascript theme={null}
import { sign } from '@noble/ed25519';
import { Buffer } from 'buffer';

async function signRequest({ uri, method, body, privateKey, accessId }) {
  const timestamp = Date.now().toString();
  const bodyStr = body ? JSON.stringify(body) : '';

  // Construct message to sign
  const message = `${uri}:${method}:${bodyStr}:${timestamp}`;
  const messageBytes = new TextEncoder().encode(message);

  // Sign with Ed25519
  const signature = await sign(messageBytes, privateKey);
  const signatureBase64 = Buffer.from(signature).toString('base64');

  return {
    'x-access-id': accessId,
    'X-PoP-Signature': signatureBase64,
    'X-PoP-Challenge': timestamp,
    'X-PoP-Format': 'service-account',
    'Content-Type': 'application/json',
  };
}

// Usage example
const headers = await signRequest({
  uri: '/v1/account',
  method: 'GET',
  body: null,
  privateKey: process.env.ED25519_PRIVATE_KEY,
  accessId: process.env.SERVICE_ACCOUNT_ID,
});

const response = await fetch('https://conta-public-api.kiwify.com/v1/account', {
  method: 'GET',
  headers,
});
```

### Python

```python theme={null}
import time
import base64
import json
import requests
from nacl.signing import SigningKey

def sign_request(uri: str, method: str, body: dict | None, private_key_hex: str, access_id: str) -> dict:
    timestamp = str(int(time.time() * 1000))
    body_str = json.dumps(body) if body else ''

    # Construct message to sign
    message = f"{uri}:{method}:{body_str}:{timestamp}"
    message_bytes = message.encode('utf-8')

    # Sign with Ed25519
    signing_key = SigningKey(bytes.fromhex(private_key_hex))
    signed = signing_key.sign(message_bytes)
    signature_base64 = base64.b64encode(signed.signature).decode('utf-8')

    return {
        'x-access-id': access_id,
        'X-PoP-Signature': signature_base64,
        'X-PoP-Challenge': timestamp,
        'X-PoP-Format': 'service-account',
        'Content-Type': 'application/json',
    }

# Usage example
import os

headers = sign_request(
    uri='/v1/account',
    method='GET',
    body=None,
    private_key_hex=os.environ['ED25519_PRIVATE_KEY'],
    access_id=os.environ['SERVICE_ACCOUNT_ID'],
)

response = requests.get(
    'https://conta-public-api.kiwify.com/v1/account',
    headers=headers,
)
```

### cURL (with pre-computed signature)

```bash theme={null}
# Note: You'll need to compute the signature externally
# This example shows the header format

TIMESTAMP=$(date +%s000)
ACCESS_ID="your-service-account-uuid"
SIGNATURE="your-computed-base64-signature"

curl -X GET "https://conta-public-api.kiwify.com/v1/account" \
  -H "x-access-id: ${ACCESS_ID}" \
  -H "X-PoP-Signature: ${SIGNATURE}" \
  -H "X-PoP-Challenge: ${TIMESTAMP}" \
  -H "X-PoP-Format: service-account"
```

## Timestamp Validation

The `X-PoP-Challenge` timestamp must be within **5 minutes** of the server's current time. This prevents replay attacks while allowing for reasonable clock drift.

<Warning>
  Ensure your system clock is synchronized using NTP. Requests with timestamps outside the 5-minute window will be rejected with a `401 Unauthorized` error.
</Warning>

## Troubleshooting

### Common Errors

<AccordionGroup>
  <Accordion title="401 - Invalid signature">
    **Cause:** The signature doesn't match the expected value.

    **Solutions:**

    * Verify you're signing the exact message format: `{uri}:{method}:{body}:{timestamp}`
    * Ensure the URI includes the full path and query string
    * Check that the body string matches exactly what you're sending
    * Verify your private key corresponds to the public key registered with your service account
  </Accordion>

  <Accordion title="401 - Timestamp expired">
    **Cause:** The `X-PoP-Challenge` timestamp is more than 5 minutes from server time.

    **Solutions:**

    * Sync your system clock using NTP
    * Generate the timestamp immediately before making the request
    * Ensure you're using milliseconds, not seconds
  </Accordion>

  <Accordion title="401 - Invalid access ID">
    **Cause:** The `x-access-id` doesn't match any active service account.

    **Solutions:**

    * Verify the service account UUID is correct
    * Ensure the service account is active and not disabled
    * Check for typos or extra whitespace
  </Accordion>
</AccordionGroup>

### Debugging Tips

1. **Log the message before signing** - Print the exact string being signed to verify format
2. **Test with a known good signature** - Use test vectors to verify your signing implementation
3. **Check response headers** - Error responses may include additional debug information
4. **Verify key format** - Ed25519 private keys should be 32 bytes (64 hex characters)

## Security Best Practices

<CardGroup cols={2}>
  <Card title="Protect Your Private Key" icon="shield">
    Never expose your private key in client-side code, logs, or version control. Use environment variables or secure secret management.
  </Card>

  <Card title="Rotate Keys Regularly" icon="arrows-rotate">
    Generate new key pairs periodically and update your service account. Revoke old keys after rotation.
  </Card>

  <Card title="Monitor API Usage" icon="chart-line">
    Regularly review your API access logs for unexpected patterns or unauthorized attempts.
  </Card>
</CardGroup>
