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.
Investor Purchase Sessions
The issuer creates a purchase session. The issuer then redirects the investor to the returned url.
- Production:
https://invest.upside.gg/p/... - Staging:
https://invest-staging.upside.gg/p/...
Upside hosts the investor steps. The issuer receives a callback, or the issuer reads the session status.
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, purchases; GET whitelist_status, purchase 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 belongs to one asset. Use the key to whitelist investors, store KYC data, mint tokens, and create purchase sessions for that asset. A purchase session requires the purchase capability.
- 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"
}
POST /api/v1/investor/kyc
Submit KYC data without whitelisting transaction.
Request:
{
"investor": {
"id": "investor_12345",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"dob": "1990-01-15",
"fullAddress": {
"street": "123 Main St",
"city": "Berlin",
"countryCode": "DE",
"postalCode": "10115"
},
"tin": "123-45-6789",
"blockchainWallet": {
"address": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH"
},
"primaryPhone": {
"number": "+49123456789"
}
},
"callback": {
"url": "https://your-server.com/kyc-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 | 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"
}
POST /api/v1/investor/mint
Mint tokens to a wallet address.
Request:
{
"walletAddress": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH",
"amount": "1000000000",
"callback": {
"url": "https://your-server.com/mint-callback"
},
"timestamp": 1734567890123
}
| Field | Required | Description |
|---|---|---|
walletAddress | Yes | Destination wallet address (Ethereum 0x... or Solana base58) |
amount | Yes | Amount to mint (raw amount, e.g., in smallest unit). For 6 decimals, "1000000000" = 1000 tokens. Can be string, number, or bigint. |
callback.url | Yes | URL to receive status callbacks |
timestamp | Yes | Request timestamp (ms since epoch) |
Response (201 Created):
{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"status": "QUEUED",
"message": "Mint request accepted and queued for processing"
}
GET /api/v1/investor/kyc/:wallet_address
Read KYC records for a wallet address. Requires Cognito JWT with transfer-agent group membership.
Request Headers:
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <cognito_access_token> |
Example:
curl https://ta-staging.upside.gg/api/v1/investor/kyc/HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH \
-H "Authorization: Bearer YOUR_COGNITO_ACCESS_TOKEN"
Response (200 OK):
{
"success": true,
"walletAddress": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH",
"records": [
{
"subjectId": "investor_12345:HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH",
"account": {
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"dob": "1990-01-15",
"blockchainWallet": { "address": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH" }
},
"fileUrls": {
"identityDocument": "https://presigned-s3-url...",
"proofOfAddress": "https://presigned-s3-url..."
}
}
],
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
| Field | Description |
|---|---|
records | Array of KYC records for the wallet (may be empty) |
records[].account | Investor PII data |
records[].fileUrls | Presigned S3 URLs for documents (1-minute expiry) |
Response (401): Missing or invalid JWT token.
Response (403): Valid JWT but user is not in the transfer-agent group.
GET /api/v1/investor/whitelist_status/:wallet_address
Check the whitelist status of a wallet address. Requires X-Api-Key header (no signature required).
Request Headers:
| Header | Required | Description |
|---|---|---|
X-Api-Key | Yes | Your API key (Ed25519 public key, base64-encoded) |
Example:
curl https://ta-staging.upside.gg/api/v1/investor/whitelist_status/HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH \
-H "X-Api-Key: YOUR_BASE64_PUBLIC_KEY"
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 |
POST /api/v1/investor/purchases
Create an investor purchase session. The API key must have the purchase capability. This request requires X-Api-Key and X-Request-Sig-B64.
Upside hosts the investor steps. Redirect the investor to the returned url.
Request:
{
"externalInvestorId": "investor_12345",
"email": "alice@example.com",
"walletAddress": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH",
"amount": "1000",
"paymentMethod": "usd",
"returnUrl": "https://your-server.com/purchase-return",
"callbackUrl": "https://your-server.com/purchase-callback",
"timestamp": 1734567890123
}
| Field | Required | Description |
|---|---|---|
externalInvestorId | No | Your identifier for the investor |
email | No | Investor email address |
walletAddress | No | Wallet address to prefill. A wallet that is not on the asset chain is not saved |
amount | No | Investment amount as a decimal string. Send this field with paymentMethod to prefill the order. An amount that the offering does not accept is not saved |
paymentMethod | No | usd or usdc. Send this field with amount to prefill the order |
returnUrl | No | URL that the investor returns to after the session |
callbackUrl | No | HTTPS URL that receives the purchase result |
timestamp | Yes | Request timestamp (ms since epoch) |
Response (201 Created):
{
"purchaseId": "pur-1734567890123-a1b2c3d4",
"url": "https://invest-staging.upside.gg/p/SESSION_TOKEN",
"expiresAt": "2026-10-16T12:00:00.000Z"
}
| Field | Description |
|---|---|
purchaseId | Purchase session identifier. Use this value to read the status |
url | Investor session URL. Production uses https://invest.upside.gg/p/.... Staging uses https://invest-staging.upside.gg/p/... |
expiresAt | Time when the session expires (ISO 8601) |
Response (400): The offering is inactive.
{
"error": "OFFERING_INACTIVE",
"details": "This asset has no active offering"
}
GET /api/v1/investor/purchases/:purchaseId
Read the status of a purchase session. Requires X-Api-Key (no signature required). The API key must have the purchase capability.
Example:
curl https://ta-staging.upside.gg/api/v1/investor/purchases/pur-1734567890123-a1b2c3d4 \
-H "X-Api-Key: YOUR_BASE64_PUBLIC_KEY"
Response (200 OK):
{
"purchaseId": "pur-1734567890123-a1b2c3d4",
"status": "minted",
"currentStep": "complete",
"order": {
"tokenAmount": "100",
"subtotal": "1000",
"fee": "10",
"total": "1010",
"currency": "USD",
"paymentMethod": "usd"
},
"wallet": {
"chainFamily": "solana",
"address": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH",
"verification": "signature"
},
"whitelist": {
"txHash": "5UBsqwhitelistabc123"
},
"mint": {
"txHash": "5UBsqmintabc123"
}
}
| Field | Description |
|---|---|
status | created, in_progress, paid, whitelisting, whitelisted, minting, minted, failed, cancelled, or expired |
currentStep | amount, wallet, identity, accreditation, agreement, payment, minting, or complete |
order | Order summary: tokenAmount, subtotal, fee, total, currency, paymentMethod |
wallet | chainFamily (evm, solana, sui), address, and verification (signature or manual) |
whitelist.txHash | Whitelist transaction hash |
mint.txHash | Mint transaction hash |
failure | Present when status is failed: step, code, message |
Response (404): The purchase session does not exist.
GET /api/v1/callback_pk
Get the Ed25519 public key used to sign callback payloads (no authentication required).
Response (200 OK):
Returns the base64-encoded public key as plain text.
curl https://ta-staging.upside.gg/api/v1/callback_pk
# Returns: abc123...base64-encoded-public-key...
Use this key to verify the X-Callback-Sig-B64 header on callbacks.
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": "pur-1734567890123-a1b2c3d4",
"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 canonicalized callback body |
Content-Type | application/json |
Verifying Callback Signatures
Callbacks are signed using Ed25519. To verify the signature:
- Fetch the public key from
GET /api/v1/callback_pk(can be cached) - Canonicalize the callback body (JSON with sorted keys)
- Verify the
X-Callback-Sig-B64header against the canonicalized body
# Get the callback signer public key
curl https://ta-staging.upside.gg/api/v1/callback_pk
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 |
WHITELIST_UNSUPPORTED | Automated whitelist is not available for this chain | No |
MINT_UNSUPPORTED | Automated mint is not available for this chain | No |
OPERATOR_KEY_INVALID | The offering operator key is missing or not valid | No |
Automated whitelist and mint are available for Solana assets and for EVM v5 assets. EVM v3 can whitelist but cannot mint, and that mint ends in FAILED with MINT_UNSUPPORTED. Sui ends in FAILED with WHITELIST_UNSUPPORTED.
Purchase Callback Payload
When a purchase session completes, Upside sends this payload to callbackUrl. requestId is the purchase id. clientRequestSignature is also the purchase id.
{
"requestId": "pur-1734567890123-a1b2c3d4",
"status": "SUCCESS",
"reason": null,
"txId": "5UBsqmintabc123",
"message": "minted",
"clientRequestSignature": "pur-1734567890123-a1b2c3d4"
}
| Field | Description |
|---|---|
requestId | Purchase session identifier |
status | SUCCESS or FAILED |
reason | Failure code when status is FAILED. Includes WHITELIST_UNSUPPORTED, MINT_UNSUPPORTED, OPERATOR_KEY_INVALID, and BROADCAST_FAILED |
txId | Mint transaction id when status is SUCCESS |
message | Human-readable status message |
clientRequestSignature | Purchase session identifier. Use this value to detect a duplicate callback |
Verify X-Callback-Sig-B64 with the key from GET /api/v1/callback_pk.
Purchase Status Values
The status check returns these purchase session values. The callback status is SUCCESS or FAILED.
| Status | Description | Final? |
|---|---|---|
created | The session exists. The investor has not started | No |
in_progress | The investor is completing the steps | No |
paid | The investor has paid | No |
whitelisting | Upside is whitelisting the wallet | No |
whitelisted | The wallet is whitelisted | No |
minting | Upside is minting the tokens | No |
minted | The tokens are minted | Yes |
failed | The session failed. Read failure for the step and the code | Yes |
cancelled | The session is cancelled | Yes |
expired | The session expired | Yes |
Handling Callbacks
import express from 'express';
import { sign } from 'tweetnacl';
import canonicalize from 'canonicalize';
const app = express();
// Fetch this once from GET /api/v1/callback_pk and cache it
const CALLBACK_PUBLIC_KEY = Buffer.from('BASE64_PUBLIC_KEY_FROM_CALLBACK_PK_ENDPOINT', 'base64');
function verifyCallbackSignature(body: object, signatureB64: string): boolean {
const canonicalBody = canonicalize(body);
const signature = Buffer.from(signatureB64, 'base64');
return sign.detached.verify(
Buffer.from(canonicalBody, 'utf-8'),
signature,
CALLBACK_PUBLIC_KEY
);
}
app.post('/whitelist-callback', express.json(), (req, res) => {
// 1. Verify callback signature (recommended)
const signatureHeader = req.headers['x-callback-sig-b64'] as string;
if (!signatureHeader || !verifyCallbackSignature(req.body, signatureHeader)) {
console.error('Invalid callback signature');
return res.status(401).send('Invalid signature');
}
// 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 Examples
Whitelisting 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',
firstName: 'Alice',
lastName: 'Doe',
email: 'alice@example.com',
dob: '1990-01-15',
fullAddress: {
street: '123 Main St',
city: 'Berlin',
countryCode: 'DE',
postalCode: '10115',
},
tin: '123-45-6789',
blockchainWallet: {
address: 'HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH',
},
primaryPhone: {
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",
"firstName": "Alice",
"lastName": "Doe",
"email": "alice@example.com",
"dob": "1990-01-15",
"fullAddress": {
"street": "123 Main St",
"city": "Berlin",
"countryCode": "DE",
"postalCode": "10115"
},
"tin": "123-45-6789",
"blockchainWallet": {
"address": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH"
},
"primaryPhone": {
"number": "+49123456789"
}
},
"callback": {
"url": "https://your-server.com/callback"
},
"timestamp": 1734567890123
}'
Minting 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 mintTokens(walletAddress: string, amount: string, callbackUrl: string) {
const timestamp = Date.now();
const body = {
walletAddress,
amount,
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/mint`, {
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 - mint 1000 tokens (with 6 decimals, so 1000000000 raw units)
const result = await mintTokens(
'HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH',
'1000000000',
'https://your-server.com/mint-callback',
);
curl -X POST https://ta-staging.upside.gg/api/v1/investor/mint \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_BASE64_PUBLIC_KEY" \
-H "X-Request-Sig-B64: YOUR_BASE64_SIGNATURE" \
-d '{
"walletAddress": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH",
"amount": "1000000000",
"callback": {
"url": "https://your-server.com/mint-callback"
},
"timestamp": 1734567890123
}'
Purchase Session Example
- TypeScript
- cURL
import express from 'express';
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');
function signBody(body: object): string {
const canonicalBody = canonicalize(body);
const signature = sign.detached(
Buffer.from(canonicalBody, 'utf-8'),
SECRET_KEY
);
return Buffer.from(signature).toString('base64');
}
async function createPurchaseSession() {
const body = {
externalInvestorId: 'investor_12345',
email: 'alice@example.com',
walletAddress: 'HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH',
amount: '1000',
paymentMethod: 'usd',
returnUrl: 'https://your-server.com/purchase-return',
callbackUrl: 'https://your-server.com/purchase-callback',
timestamp: Date.now(),
};
const response = await fetch(`${API_URL}/api/v1/investor/purchases`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': API_KEY,
'X-Request-Sig-B64': signBody(body),
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Request failed: ${await response.text()}`);
}
const created = await response.json();
// Redirect the investor to created.url.
// Production: https://invest.upside.gg/p/...
// Staging: https://invest-staging.upside.gg/p/...
return created;
}
async function getPurchaseStatus(purchaseId: string) {
const response = await fetch(
`${API_URL}/api/v1/investor/purchases/${purchaseId}`,
{
headers: { 'X-Api-Key': API_KEY },
}
);
if (!response.ok) {
throw new Error(`Request failed: ${await response.text()}`);
}
return response.json();
}
// Use the same callback public key as the whitelist example.
const app = express();
app.post('/purchase-callback', express.json(), async (req, res) => {
// verifyCallbackSignature is the function in Handling Callbacks.
const signatureHeader = req.headers['x-callback-sig-b64'] as string;
if (!signatureHeader || !verifyCallbackSignature(req.body, signatureHeader)) {
return res.status(401).send('Invalid signature');
}
const { requestId, status, reason, txId, clientRequestSignature } = req.body;
if (isAlreadyProcessed(clientRequestSignature)) {
return res.status(200).send('OK');
}
if (status === 'SUCCESS') {
markPurchaseMinted(requestId, txId);
} else {
handlePurchaseFailure(requestId, reason);
}
res.status(200).send('OK');
});
// Read the status when the callback does not arrive.
const status = await getPurchaseStatus('pur-1734567890123-a1b2c3d4');
# 1. Create the session
curl -X POST https://ta-staging.upside.gg/api/v1/investor/purchases \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_BASE64_PUBLIC_KEY" \
-H "X-Request-Sig-B64: YOUR_BASE64_SIGNATURE" \
-d '{
"externalInvestorId": "investor_12345",
"email": "alice@example.com",
"walletAddress": "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH",
"amount": "1000",
"paymentMethod": "usd",
"returnUrl": "https://your-server.com/purchase-return",
"callbackUrl": "https://your-server.com/purchase-callback",
"timestamp": 1734567890123
}'
# 2. Redirect the investor to the returned url
# Production: https://invest.upside.gg/p/...
# Staging: https://invest-staging.upside.gg/p/...
# 3. Read the status when the callback does not arrive
curl https://ta-staging.upside.gg/api/v1/investor/purchases/pur-1734567890123-a1b2c3d4 \
-H "X-Api-Key: YOUR_BASE64_PUBLIC_KEY"
Next Steps
- API Reference - Interactive OpenAPI documentation