Skip to main content

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
  • IP binding - Requests are validated against your configured IP allowlist

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
  4. IP Allowlist - Configure allowed IP addresses for your service account

Required Headers

Every authenticated request must include these 5 headers:
HeaderDescriptionExample
x-access-idYour service account UUID550e8400-e29b-41d4-a716-446655440000
X-PoP-SignatureEdDSA signature (base64)MEUCIQDx...
X-PoP-ChallengeUnix timestamp in milliseconds1705423200000
X-PoP-FormatAuthentication type identifierservice-account
true-client-ipYour client’s IP address203.0.113.50

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}
ComponentDescription
uriFull request path including query string (e.g., /v1/account?include=balance)
methodHTTP method in uppercase (e.g., GET, POST)
bodyRequest body as string, or empty string for GET requests
timestampSame 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

import { sign } from '@noble/ed25519';
import { Buffer } from 'buffer';

async function signRequest({ uri, method, body, privateKey, accessId, clientIp }) {
  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',
    'true-client-ip': clientIp,
    '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,
  clientIp: '203.0.113.50',
});

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

Python

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, client_ip: 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',
        'true-client-ip': client_ip,
        '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'],
    client_ip='203.0.113.50',
)

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

cURL (with pre-computed signature)

# 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"
CLIENT_IP="203.0.113.50"

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" \
  -H "true-client-ip: ${CLIENT_IP}"

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.
Ensure your system clock is synchronized using NTP. Requests with timestamps outside the 5-minute window will be rejected with a 401 Unauthorized error.

IP Allowlist

The true-client-ip header value must match one of the IP addresses configured in your service account’s allowlist. This provides an additional layer of security. To configure your IP allowlist:
  1. Go to your Conta Digital dashboard
  2. Navigate to Settings > Service Accounts
  3. Edit your service account
  4. Add allowed IP addresses (IPv4 or IPv6)

Troubleshooting

Common Errors

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
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
Cause: The true-client-ip doesn’t match your service account’s IP allowlist.Solutions:
  • Verify your current public IP address
  • Update your service account’s IP allowlist in the dashboard
  • If behind a proxy/NAT, use the external IP address
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

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

Protect Your Private Key

Never expose your private key in client-side code, logs, or version control. Use environment variables or secure secret management.

Rotate Keys Regularly

Generate new key pairs periodically and update your service account. Revoke old keys after rotation.

Minimize IP Allowlist

Only allow IPs that genuinely need API access. Use specific IPs rather than wide CIDR ranges.

Monitor API Usage

Regularly review your API access logs for unexpected patterns or unauthorized attempts.