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.
Whitelist Status Check
Query the current whitelist status of any wallet address.
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
Request Headers
| Header | Required | Description |
|---|---|---|
X-Api-Key | Yes | Your API key (Ed25519 public key, base64-encoded) |
X-Request-Sig-B64 | POST only | Base64-encoded Ed25519 signature of the canonicalized request body |
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.
API Endpoints
POST /api/v1/investor/whitelist
Submit an investor for whitelisting.
Request:
{
"investor": {
"id": "investor_12345",
"first_name": "Alice",
"last_name": "Doe",
"email": "alice@example.com",
"dob": "1990-01-15",
"full_address": {
"street": "123 Main St",
"city": "Berlin",
"region": "BE",
"countryCode": "DE",
"postalCode": "10115"
},
"tin": "123-45-6789",
"blockchain_wallet": {
"address": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH"
},
"primary_phone": {
"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 |
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) |
Response (201 Created):
{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"status": "QUEUED",
"message": "Whitelisting request accepted and queued for processing"
}
POST /api/v1/investor/kyc
Submit KYC data without whitelisting transaction.
Request:
{
"account": {
"id": "investor_12345",
"first_name": "Alice",
"last_name": "Doe",
"email": "alice@example.com",
"dob": "1990-01-15",
"full_address": {
"street": "123 Main St",
"city": "Berlin",
"countryCode": "DE",
"postalCode": "10115"
},
"tin": "123-45-6789",
"blockchain_wallet": {
"address": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH"
},
"primary_phone": {
"number": "+49123456789"
}
},
"callback": {
"url": "https://your-server.com/kyc-callback"
},
"timestamp": 1734567890123,
"verifiedStatus": true,
"verifiedAt": "2024-01-15T10:30:00.000Z"
}
| Field | Required | Description |
|---|---|---|
account | Yes | Account/investor PII data |
callback.url | Yes | URL to receive status callbacks |
timestamp | Yes | Request timestamp (ms since epoch) |
verifiedStatus | Yes | KYC verification status |
verifiedAt | Yes | KYC verification timestamp (ISO 8601) |
Response (201 Created):
{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"status": "QUEUED",
"message": "KYC request accepted and queued for processing"
}
GET /api/v1/investor/whitelist_status/:wallet_address
Check the whitelist status of a wallet address.
Response (200 OK):
{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"address": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH",
"isWhitelisted": true,
"whitelistGroupId": 1,
"walletGroup": 1
}
| Field | Description |
|---|---|
isWhitelisted | true if wallet is in the expected whitelist group |
whitelistGroupId | The expected group from API key configuration |
walletGroup | The actual group the wallet belongs to on-chain |
GET /api/v1/health
Health check endpoint (no authentication required).
Response (200 OK):
{
"status": "ok",
"timestamp": "2024-12-15T10:30:00.000Z"
}
Callbacks
When processing completes, a callback is delivered to your specified URL.
Callback Payload
{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"status": "SUCCESS",
"reason": null,
"txId": "5UBsq...abc123",
"message": "Investor successfully whitelisted",
"clientRequestSignature": "base64-signature-from-X-Request-Sig-B64-header"
}
| Field | Description |
|---|---|
requestId | Unique request identifier |
status | SUCCESS, FAILED, or PENDING |
reason | Error code if failed (see below) |
txId | Blockchain transaction ID (on success) |
message | Human-readable status message |
clientRequestSignature | Your original X-Request-Sig-B64 header echoed back |
Callback Headers
| Header | Description |
|---|---|
X-Callback-Sig-B64 | Base64-encoded Ed25519 signature of the callback body |
Content-Type | application/json |
Status Values
| Status | Description | Final? |
|---|---|---|
QUEUED | Request accepted and queued for processing | No |
PENDING | Request is being processed | No |
SUCCESS | Whitelisting completed successfully | Yes |
FAILED | Whitelisting failed (see reason) | Yes |
Failure Reasons
| Reason | Description | Retryable? |
|---|---|---|
INVALID_MESSAGE | Queue message malformed or failed validation | No |
PII_STORAGE_ERROR | Failed to store investor KYC data | Yes |
ALREADY_WHITELISTED | Wallet is already in the target whitelist group | No |
SIGNING_NOT_ALLOWED | API key lacks permission for this token/network | No |
SIGNING_ERROR | Transaction signing failed | Yes |
BROADCAST_SUBMIT_ERROR | Failed to submit transaction to relayer | Yes |
BROADCAST_FAILED | Transaction submitted but failed on-chain | Maybe |
CALLBACK_DELIVERY_FAILED | Callback couldn't be delivered to your URL | N/A |
Handling Callbacks
import express from 'express';
import { sign } from 'tweetnacl';
import canonicalize from 'canonicalize';
const app = express();
const CALLBACK_PUBLIC_KEY = Buffer.from('UPSIDE_CALLBACK_PUBLIC_KEY', 'base64');
app.post('/whitelist-callback', express.json(), (req, res) => {
// 1. Verify callback signature (optional but recommended)
const signature = req.headers['x-callback-sig-b64'];
// ... verify signature using CALLBACK_PUBLIC_KEY
// 2. Check for duplicate using clientRequestSignature (idempotency)
const { clientRequestSignature, status, reason, txId, requestId } = req.body;
if (isAlreadyProcessed(clientRequestSignature)) {
return res.status(200).send('OK');
}
// 3. Process the callback
switch (status) {
case 'SUCCESS':
markAsWhitelisted(requestId, txId);
break;
case 'FAILED':
handleFailure(requestId, reason);
break;
}
// 4. Acknowledge receipt
res.status(200).send('OK');
});
Status Check Fallback
If you miss a callback, use the status check endpoint:
GET /api/v1/investor/whitelist_status/{wallet_address}
This returns the current on-chain whitelist status (source of truth).
Full Example
- TypeScript
- cURL
import { sign } from 'tweetnacl';
import canonicalize from 'canonicalize';
const API_URL = 'https://ta-staging.upside.gg';
const API_KEY = 'YOUR_BASE64_PUBLIC_KEY';
const SECRET_KEY = Buffer.from('YOUR_BASE64_SECRET_KEY', 'base64');
async function whitelistInvestor(investor: object, callbackUrl: string) {
const timestamp = Date.now();
const body = {
investor,
callback: { url: callbackUrl },
timestamp,
};
// Sign the request
const canonicalBody = canonicalize(body);
const signature = sign.detached(
Buffer.from(canonicalBody, 'utf-8'),
SECRET_KEY
);
const signatureBase64 = Buffer.from(signature).toString('base64');
// Send the request
const response = await fetch(`${API_URL}/api/v1/investor/whitelist`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': API_KEY,
'X-Request-Sig-B64': signatureBase64,
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Request failed: ${await response.text()}`);
}
return response.json();
}
// Usage
const result = await whitelistInvestor(
{
id: 'investor_12345',
first_name: 'Alice',
last_name: 'Doe',
email: 'alice@example.com',
dob: '1990-01-15',
full_address: {
street: '123 Main St',
city: 'Berlin',
countryCode: 'DE',
postalCode: '10115',
},
tin: '123-45-6789',
blockchain_wallet: {
address: 'HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH',
},
primary_phone: {
number: '+49123456789',
},
},
'https://your-server.com/callback',
);
curl -X POST https://ta-staging.upside.gg/api/v1/investor/whitelist \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_BASE64_PUBLIC_KEY" \
-H "X-Request-Sig-B64: YOUR_BASE64_SIGNATURE" \
-d '{
"investor": {
"id": "investor_12345",
"first_name": "Alice",
"last_name": "Doe",
"email": "alice@example.com",
"dob": "1990-01-15",
"full_address": {
"street": "123 Main St",
"city": "Berlin",
"countryCode": "DE",
"postalCode": "10115"
},
"tin": "123-45-6789",
"blockchain_wallet": {
"address": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH"
},
"primary_phone": {
"number": "+49123456789"
}
},
"callback": {
"url": "https://your-server.com/callback"
},
"timestamp": 1734567890123
}'
Next Steps
- API Reference - Interactive OpenAPI documentation