Skip to main content
The Issuing API uses JSON for request and response bodies, and requires cryptographic signatures for authentication. This guide covers everything you need to know about making requests and handling responses.

Quick Start

All API requests follow this pattern:
  1. Format - JSON request body with Content-Type: application/json
  2. Authenticate - Sign requests using RSA-SHA256 JWT tokens
  3. Handle - Process responses and errors appropriately

Request Format

Content Type

All requests must use JSON encoding:
  • Content-Type: application/json
  • Request bodies must be valid, well-formed JSON
  • All responses are returned in JSON format

Required Headers

Every API request must include these headers:
HeaderRequiredDescriptionExample
Content-TypeYesMust be application/jsonapplication/json
AuthorizationYesBearer token with signed JWTBearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Idempotency-KeyConditionalUUID for POST requests (recommended)550e8400-e29b-41d4-a716-446655440000
Idempotency Key: Always include an Idempotency-Key header for POST requests. This ensures safe retries and prevents duplicate operations. See Idempotency for details.

Authentication

All API requests require cryptographic signatures:
  • SHA-256 - For hashing request bodies
  • RSA - For signing JWT tokens
See the Authentication guide for detailed signing instructions.

Example Request

curl -X POST https://access.utgl.io/v1/accounts \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "name": "My Account",
    "currency": "USD"
  }'

Response Format

Response Headers

All API responses include standard HTTP headers plus:
HeaderDescriptionExample
x-request-idUnique identifier for this request. Always log this for support purposes550e8400-e29b-41d4-a716-446655440000
Content-TypeResponse content type (always application/json)application/json
Retry-AfterPresent on 429 responses, indicates seconds to wait60

Success Responses

Successful requests return:
  • HTTP Status: 200 OK (or 201 Created for resource creation)
  • Body: JSON-encoded response data
Example Success Response:
{
  "id": "acc_1234567890",
  "name": "My Account",
  "currency": "USD",
  "balance": "1000.00",
  "status": "active",
  "createdAt": "2024-01-15T10:30:00Z"
}

Response Structure

Response bodies follow consistent patterns:
  • Single Resource: Object with resource properties
  • List Resources: Object with data array and pagination metadata
  • Empty Response: {} or null for DELETE operations

HTTP Status Codes

The API uses standard HTTP status codes to communicate request outcomes:
Status CodeCategoryMeaningAction Required
200SuccessRequest completed successfullyNone - process response data
201SuccessResource created successfullyNone - process response data
400Client ErrorInvalid request format or parametersFix request before retrying
401Client ErrorAuthentication failedCheck credentials and signature
403Client ErrorAccess deniedVerify permissions and account status
404Client ErrorResource not foundVerify resource ID exists
409Client ErrorResource conflict (e.g., duplicate)Check for existing resources
429Client ErrorRate limit exceededWait and retry with backoff
500Server ErrorInternal server errorRetry with exponential backoff
502/503/504Server ErrorService temporarily unavailableRetry after delay

Status Code Details

200 OK

The request succeeded. Response body contains the requested data.

201 Created

Resource was successfully created. Response body contains the new resource.

400 Bad Request

The request is malformed or contains invalid parameters. Check the error response for specific issues. Common causes:
  • Missing required fields
  • Invalid field formats
  • Malformed JSON
  • Invalid enum values

401 Unauthorized

Authentication failed. The access key or JWT signature is invalid. Common causes:
  • Invalid or missing access key
  • Incorrect JWT signature
  • Missing Authorization header
  • Signature verification failure
  • JWT token expired (must be less than 30 seconds old)
  • URI or method claims don’t match the request

403 Forbidden

Access denied. Your credentials are valid but you don’t have permission. Common causes:
  • Account is inactive
  • Subscription doesn’t include this feature
  • IP address not whitelisted
  • Invalid account number or unique_id
  • Insufficient permissions

404 Not Found

The requested resource doesn’t exist or isn’t accessible. Common causes:
  • Resource ID is incorrect
  • Resource belongs to another account
  • Resource has been deleted

409 Conflict

The request conflicts with the current state of the resource. Common causes:
  • Duplicate resource (e.g., card with same reference)
  • Resource already exists
  • Concurrent modification conflict

429 Too Many Requests

Rate limit exceeded. Too many requests in a given time period. Response includes:
  • Retry-After header indicating seconds to wait
  • Error details in response body
See Rate Limiting for details.

500 Internal Server Error

A server-side error occurred. This is typically temporary. Action: Retry the request with exponential backoff.

502/503/504 Service Unavailable

The service is temporarily unavailable or undergoing maintenance. Action: Retry after a delay. Check status page for maintenance windows.

Error Responses

When an error occurs, the API returns an appropriate HTTP status code with a structured error response.

Error Response Format

All error responses follow this structure:
{
  "code": "INSUFFICIENT_BALANCE",
  "message": "The funding account has insufficient balance to complete the transfer. Create a Transfer to fund the source account."
}
Some errors may include additional context:
{
  "code": "INVALID_PARAMETERS",
  "message": "Validation failed",
  "details": {
    "field": "amount",
    "reason": "must_be_positive"
  }
}

Error Response Fields

FieldTypeDescriptionExample
codestringMachine-readable error code for programmatic handlingINSUFFICIENT_BALANCE
messagestringHuman-readable error description with guidanceThe source account has insufficient balance...
detailsobjectAdditional error context (optional, varies by error){"field": "amount", "reason": "must_be_positive"}
Important: Always use the code field for programmatic error handling. The message field is for human readability and may change over time.

Common Error Codes by Status

Authentication Errors (401)

{
  "code": "INVALID_SIGNATURE",
  "message": "JWT signature verification failed"
}
Solutions:
  • Verify your access key is correct
  • Check the Authorization header is properly formatted
  • Verify your JWT signature is correct
  • Check that the uri and method claims match the request exactly
  • Ensure the JWT token hasn’t expired (must be less than 30 seconds old)

Validation Errors (400)

{
  "code": "INVALID_PARAMETERS",
  "message": "Validation failed",
  "details": {
    "field": "amount",
    "reason": "must_be_positive"
  }
}
Solutions:
  • Check all required fields are present
  • Verify field formats match the API specification
  • Ensure numeric values are within valid ranges
  • Review validation rules in the API Reference

Insufficient Balance (400)

{
  "code": "INSUFFICIENT_BALANCE",
  "message": "The funding account has insufficient balance to complete the transfer. Create a Transfer to fund the source account."
}
Solutions:
  • Check account balance before initiating transfers
  • Fund the account using the appropriate endpoint
  • Verify currency matches between accounts

Resource Not Found (404)

{
  "code": "INVALID_CARD_ACCOUNT",
  "message": "The card account is invalid or does not exist."
}
Solutions:
  • Verify the resource ID is correct
  • Ensure the resource belongs to your account
  • Check if the resource has been deleted
  • Verify you’re using the correct endpoint

Rate Limit Errors (429)

{
  "code": "RATE_LIMIT_EXCEEDED",
  "message": "Too many requests. Please try again later."
}
The Retry-After header will be included in the HTTP response headers, not in the error body.
Solutions:
  • Implement exponential backoff
  • Respect the Retry-After header
  • Review your rate limits
  • Consider batching requests when possible

Conflict Errors (409)

{
  "code": "INVALID_PARAMETERS",
  "message": "A card with this reference already exists"
}
Solutions:
  • Check for existing resources before creating
  • Use idempotency keys for safe retries
  • Verify unique constraints in the API specification

Common Error Codes

Error CodeHTTP StatusDescription
INVALID_PARAMETERS400Invalid request parameters
INVALID_ACCOUNT400Invalid account identifier
INVALID_CARD_ACCOUNT400Invalid card account identifier
INSUFFICIENT_BALANCE400Account doesn’t have sufficient balance
INVALID_SIGNATURE401JWT signature verification failed
RATE_LIMIT_EXCEEDED429Too many requests
For a complete list of error codes, refer to the API Reference. Error codes are defined in the OpenAPI specification for each endpoint.

Error Handling Best Practices

1. Check HTTP Status Codes

Always check the HTTP status code first:
if (response.status >= 400) {
  const error = await response.json();
  console.error('API Error:', {
    code: error.code,
    message: error.message,
    status: response.status
  });
  // Handle error based on code
}

2. Implement Retry Logic

Retry only for transient failures (5xx, 429):
async function apiCall(url, options, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      
      // Don't retry client errors (4xx)
      if (response.status >= 400 && response.status < 500) {
        const error = await response.json();
        throw new APIError(error.code, error.message, response.status);
      }
      
      // Retry server errors (5xx)
      if (response.status >= 500 && i < maxRetries - 1) {
        await sleep(Math.pow(2, i) * 1000); // Exponential backoff
        continue;
      }
      
      return await response.json();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
    }
  }
}

3. Log Request IDs

Always log the x-request-id from error responses:
try {
  const result = await createCard(data);
} catch (error) {
  const requestId = error.response?.headers?.get('x-request-id');
  console.error('Error creating card:', {
    requestId,
    code: error.code,
    message: error.message,
    status: error.status
  });
  
  // Report to your error tracking service
  reportError(error, requestId);
}

4. Handle Rate Limits Gracefully

Respect rate limits and retry after the specified time:
async function apiCallWithRateLimit(url, options) {
  const response = await fetch(url, options);
  
  if (response.status === 429) {
    const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
    await sleep(retryAfter * 1000);
    return apiCallWithRateLimit(url, options); // Retry
  }
  
  return response.json();
}

5. Display User-Friendly Messages

Don’t expose raw API errors to end users:
function getUserFriendlyMessage(error) {
  const messages = {
    'INSUFFICIENT_BALANCE': 'You don't have enough balance for this transaction.',
    'INVALID_CARD_ACCOUNT': 'The requested card account was not found.',
    'INVALID_PARAMETERS': 'Please check your input and try again.',
    'RATE_LIMIT_EXCEEDED': 'We're experiencing high traffic. Please try again in a moment.',
    'INVALID_SIGNATURE': 'Authentication failed. Please check your credentials.'
  };
  
  return messages[error.code] || 'Something went wrong. Please try again.';
}

Retrying Requests

Not all failed requests should be retried. Implement retry logic only for specific scenarios.

When to Retry

Retry these status codes:
  • 5xx (Server errors) - Temporary server-side issues
  • 429 (Rate Limits) - Rate limit exceeded, wait and retry
Do NOT retry these status codes:
  • 400 (Bad Request) - Fix the request first
  • 401 (Unauthorized) - Check authentication
  • 403 (Forbidden) - Verify permissions
  • 404 (Not Found) - Resource doesn’t exist
  • 409 (Conflict) - Resolve conflict first

Retry Implementation

Exponential Backoff

Implement exponential backoff with jitter to prevent thundering herd problems:
async function retryWithBackoff(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      // Don't retry client errors (4xx)
      if (error.status >= 400 && error.status < 500) {
        throw error;
      }
      
      // Last attempt
      if (i === maxRetries - 1) {
        throw error;
      }
      
      // Exponential backoff with jitter
      const baseDelay = Math.pow(2, i) * 1000; // 1s, 2s, 4s...
      const jitter = Math.random() * 1000; // 0-1s random
      await sleep(baseDelay + jitter);
    }
  }
}

Respect Rate Limits

When receiving a 429 response, respect the Retry-After header:
async function handleRateLimit(response) {
  if (response.status === 429) {
    const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
    await sleep(retryAfter * 1000);
    return true; // Retry
  }
  return false;
}

Idempotency Keys

Always use a new idempotency key for each retry attempt:
async function makeRequestWithRetry(data) {
  let attempt = 0;
  const maxRetries = 3;
  
  while (attempt < maxRetries) {
    const idempotencyKey = generateUUID(); // New key each time
    const headers = {
      'Idempotency-Key': idempotencyKey,
      // ... other headers
    };
    
    try {
      return await fetch(url, { headers, body: JSON.stringify(data) });
    } catch (error) {
      if (shouldRetry(error.status)) {
        attempt++;
        await exponentialBackoff(attempt);
        continue;
      }
      throw error;
    }
  }
}
Critical: When retrying, always provide a new idempotency key. Requests with the same idempotency key return the same result, including errors. This ensures retries are treated as new attempts.

Request ID Tracking

Every API response includes an x-request-id header. This unique identifier is essential for:
  • Debugging - Track specific requests in your logs
  • Support - Reference requests when contacting support
  • Troubleshooting - Correlate requests across systems
  • Auditing - Track API usage and issues

Best Practices

Always log the request ID:
async function makeAPIRequest(url, options) {
  const response = await fetch(url, options);
  const requestId = response.headers.get('x-request-id');
  
  // Log for debugging
  console.log(`Request ID: ${requestId}`);
  
  if (!response.ok) {
    const error = await response.json();
    // Include request ID in error logs
    logger.error('API Error', {
      requestId,
      status: response.status,
      error: error.code,
      message: error.message
    });
  }
  
  return response;
}
Include in support requests: When contacting support, always include:
  • The x-request-id from the response
  • Timestamp of the request
  • Endpoint and method used
  • Request payload (sanitized of sensitive data)

Best Practices

Request Formatting

  1. Validate JSON - Ensure request bodies are valid JSON before sending
  2. Use consistent formatting - Follow the API specification exactly
  3. Include all required fields - Check endpoint documentation
  4. Use proper data types - Numbers as numbers, not strings (unless specified)

Error Handling

  1. Check status codes first - Handle different status codes appropriately
  2. Use error codes, not messages - Parse the code field for programmatic handling
  3. Log request IDs - Always capture x-request-id for debugging
  4. Implement retry logic - For transient failures (5xx, 429)
  5. Don’t retry client errors - Fix 4xx errors before retrying

Performance

  1. Use idempotency keys - Prevents duplicate operations
  2. Implement connection pooling - Reuse HTTP connections
  3. Batch operations - When possible, use batch endpoints
  4. Respect rate limits - Monitor your usage and implement backoff

Security

  1. Never log sensitive data - Don’t log full request/response bodies
  2. Store credentials securely - Use environment variables or secure vaults
  3. Rotate keys regularly - Follow security best practices
  4. Validate responses - Don’t trust response data blindly

Code Examples

JavaScript/TypeScript

interface APIResponse<T> {
  data?: T;
  error?: {
    code: string;
    message: string;
    details?: any;
  };
}

async function apiRequest<T>(
  endpoint: string,
  options: RequestInit = {}
): Promise<T> {
  const url = `https://access.utgl.io/v1${endpoint}`;
  const requestId = generateUUID();
  
  const headers = {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${await generateJWT(endpoint, options.method || 'GET')}`,
    'Idempotency-Key': requestId,
    ...options.headers,
  };
  
  try {
    const response = await fetch(url, {
      ...options,
      headers,
    });
    
    const requestIdHeader = response.headers.get('x-request-id');
    logger.info('API Request', { endpoint, requestId: requestIdHeader });
    
    if (!response.ok) {
      const error = await response.json();
      throw new APIError(error.code, error.message, response.status, requestIdHeader);
    }
    
    return await response.json();
  } catch (error) {
    if (error instanceof APIError) {
      throw error;
    }
    throw new Error(`Request failed: ${error.message}`);
  }
}

Python

import requests
import uuid
from typing import Optional, Dict, Any

def api_request(
    endpoint: str,
    method: str = 'GET',
    data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
    url = f"https://access.utgl.io/v1{endpoint}"
    request_id = str(uuid.uuid4())
    
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {generate_jwt(endpoint, method, data)}',
        'Idempotency-Key': request_id,
    }
    
    try:
        response = requests.request(
            method=method,
            url=url,
            headers=headers,
            json=data,
            timeout=30
        )
        
        request_id_header = response.headers.get('x-request-id')
        logger.info(f'API Request: {endpoint}', extra={'request_id': request_id_header})
        
        response.raise_for_status()
        return response.json()
        
    except requests.exceptions.HTTPError as e:
        error_data = e.response.json() if e.response.content else {}
        raise APIError(
            code=error_data.get('code'),
            message=error_data.get('message'),
            status_code=e.response.status_code,
            request_id=request_id_header
        )

Troubleshooting

Common Issues

Issue: “Invalid JSON” errors
  • Solution: Validate JSON before sending. Ensure proper escaping of special characters.
Issue: “Signature verification failed”
  • Solution: Verify your private key is correct and the JWT is properly signed. Check the uri and method claims match exactly.
Issue: “Rate limit exceeded”
  • Solution: Implement exponential backoff and respect Retry-After headers. Review your rate limits.
Issue: “Request ID not found in logs”
  • Solution: Always capture and log the x-request-id header from responses.
Issue: “Duplicate operations”
  • Solution: Use unique idempotency keys for each request. Don’t reuse keys across different operations.


Next Steps