Getting Started
The Transfer Agent API provides a unified interface for investor whitelisting and KYC data ingestion.
Feature Overview
Investor Whitelisting
Submit investor KYC data and whitelist their wallet address on the blockchain.
KYC Data Ingestion
Store investor KYC data without whitelisting. Useful for pre-registration flows or when whitelisting is handled separately.
Token Minting
Mint tokens to a wallet address. Useful for automated token distributions on demand.
Whitelist Status Check
Query the current whitelist status of any wallet address.
KYC Read (Transfer Agent)
Read KYC/PII data for a wallet address. Requires Cognito JWT with transfer-agent group. Returns account data and presigned S3 URLs for identity documents (1-minute expiry).
Architecture Overview
Sequence Diagram
Processing Flow
- Client submits investor data via
POST /api/v1/investor/whitelist - TA API validates request and adds to processing queue
- Client receives synchronous response
201 { status: "QUEUED" }
Async processing begins:
- TA API sends investor KYC data to PII Storage
- PII Storage confirms successful storage
- TA API requests transaction signing from Signer API
- Signer API returns signed whitelist transaction
- TA API sends signed transaction to Relayer API
- Relayer API submits transaction to blockchain
- Blockchain returns transaction hash
- Relayer API returns transaction result to TA API
- TA API delivers callback with status and txId to client
Base URLs
| Environment | URL |
|---|---|
| Production | https://ta.upside.gg |
| Staging | https://ta-staging.upside.gg |
Authentication
The TA API supports two authentication methods:
| Method | Endpoints | Description |
|---|---|---|
| API Key + Signature | POST whitelist, kyc, mint; GET whitelist_status | Ed25519 public key in X-Api-Key; POST requests require X-Request-Sig-B64 signature |
| Cognito JWT | GET /api/v1/investor/kyc/:wallet_address | Bearer token from Cognito; user must be in transfer-agent group |
API Key Authentication
Request Headers
| Header | Required | Description |
|---|---|---|
X-Api-Key | Yes | Your API key (Ed25519 public key, base64-encoded). Required for all API key endpoints. |
X-Request-Sig-B64 | POST only | Base64-encoded Ed25519 signature of the canonicalized request body, verified using the public key from X-Api-Key. |
Generating API Keys
Generate an Ed25519 key pair. The public key (base64-encoded) is your API key. Share it with Upside to get access to the API.
The API key is assigned to an asset and can be used to whitelist and KYC investors for that asset.
- TypeScript
- CLI
import { sign } from 'tweetnacl';
// Generate a new key pair
const keyPair = sign.keyPair();
const publicKeyBase64 = Buffer.from(keyPair.publicKey).toString('base64');
const secretKeyBase64 = Buffer.from(keyPair.secretKey).toString('base64');
console.log('API Key (Public):', publicKeyBase64);
console.log('Secret Key (keep private!):', secretKeyBase64);
# Using OpenSSL
openssl genpkey -algorithm ed25519 -out private.pem
openssl pkey -in private.pem -pubout -out public.pem
# Extract base64-encoded public key
openssl pkey -in public.pem -pubin -outform DER | tail -c 32 | base64
Signing Requests
POST requests require Ed25519 signature verification:
- TypeScript
import { sign } from 'tweetnacl';
import canonicalize from 'canonicalize';
function signRequest(body: object, secretKey: Uint8Array): string {
const canonicalBody = canonicalize(body);
const messageBytes = Buffer.from(canonicalBody, 'utf-8');
const signature = sign.detached(messageBytes, secretKey);
return Buffer.from(signature).toString('base64');
}
// Usage
const requestBody = {
investor: { /* ... */ },
callback: { url: 'https://your-server.com/callback' },
timestamp: Date.now(),
};
const secretKey = Buffer.from('YOUR_SECRET_KEY_BASE64', 'base64');
const signature = signRequest(requestBody, secretKey);
// Send signature in X-Request-Sig-B64 header
Timestamp Validation
POST requests must include a timestamp field (Unix epoch milliseconds). The timestamp must be within ±60 seconds of server time.
Cognito JWT Authentication (Transfer Agent)
The GET /api/v1/investor/kyc/:wallet_address endpoint uses Cognito JWT authentication. Users must:
- Sign in via Cognito Hosted UI (or programmatically)
- Complete MFA (TOTP/authenticator app) — MFA is required for all users
- Be a member of the
transfer-agentgroup - Send the access token in the
Authorization: Bearer <token>header
This endpoint is intended for the Transfer Agent Dashboard and other internal tools that need to read KYC/PII data for compliance purposes.
Rate Limiting
API-key endpoints are rate limited per API key. Three fixed windows are enforced at the same time, and a request is rejected if it would exceed any of them:
| Window | Limit |
|---|---|
| Per second | 30 requests |
| Per minute | 600 requests |
| Per hour | 7,200 requests |
Spread bulk operations (e.g. whitelisting many investors) over time to stay under the limits, and treat a 429 as a signal to retry with backoff rather than an error.
API Endpoints
POST /api/v1/investor/whitelist
Submit an investor for whitelisting.
Request:
{
"investor": {
"id": "investor_12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"dob": "1990-01-15",
"fullAddress": {
"street": "123 Main St",
"city": "Berlin",
"region": "BE",
"countryCode": "DE",
"postalCode": "10115"
},
"tin": "123-45-6789",
"blockchainWallet": {
"address": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH"
},
"primaryPhone": {
"number": "+49123456789"
}
},
"callback": {
"url": "https://your-server.com/whitelist-callback"
},
"timestamp": 1734567890123,
"verifiedStatus": true,
"verifiedAt": "2024-01-15T10:30:00.000Z"
}
| Field | Required | Description |
|---|---|---|
investor | Yes | Investor PII data (see Investor Fields below) |
callback.url | Yes | URL to receive status callbacks |
timestamp | Yes | Request timestamp (ms since epoch) |
verifiedStatus | No | KYC verification status (default: true) |
verifiedAt | No | KYC verification timestamp ISO 8601 (default: current time) |
Investor Fields:
| Field | Required | Description |
|---|---|---|
id | Yes | Unique investor identifier |
firstName | Yes | Legal first name |
lastName | Yes | Legal last name |
email | Yes | Email address |
dob | Yes | Date of birth (YYYY-MM-DD) |
fullAddress | Yes | Physical address object |
tin | Yes | Tax Identification Number |
blockchainWallet | Yes | Wallet address object |
primaryPhone | Yes | Phone number object |
accreditation | No | Accreditation status: accredited, non_accredited, or pending |
ssn | No | Social Security Number (for U.S. investors) |
passportNumber | No | Passport number (for non-U.S. investors) |
identityDocumentCountry | No | ISO 3166-1 alpha-2 country code of ID issuing country |
identityDocument | No | Identity document photo (file upload) |
proofOfAddress | No | Proof of address document (if ID country differs from residence) |
File Upload Format:
{
"fileName": "passport.jpg",
"contentType": "image/jpeg",
"size": 102400,
"data": "base64-encoded-file-content"
}
Response (201 Created):
{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"status": "QUEUED",
"message": "Whitelisting request accepted and queued for processing"
}