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:
- Format - JSON request body with
Content-Type: application/json
- Authenticate - Sign requests using RSA-SHA256 JWT tokens
- Handle - Process responses and errors appropriately
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
Every API request must include these headers:
| Header | Required | Description | Example |
|---|
Content-Type | Yes | Must be application/json | application/json |
Authorization | Yes | Bearer token with signed JWT | Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... |
Idempotency-Key | Conditional | UUID 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"
}'
All API responses include standard HTTP headers plus:
| Header | Description | Example |
|---|
x-request-id | Unique identifier for this request. Always log this for support purposes | 550e8400-e29b-41d4-a716-446655440000 |
Content-Type | Response content type (always application/json) | application/json |
Retry-After | Present on 429 responses, indicates seconds to wait | 60 |
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 Code | Category | Meaning | Action Required |
|---|
| 200 | Success | Request completed successfully | None - process response data |
| 201 | Success | Resource created successfully | None - process response data |
| 400 | Client Error | Invalid request format or parameters | Fix request before retrying |
| 401 | Client Error | Authentication failed | Check credentials and signature |
| 403 | Client Error | Access denied | Verify permissions and account status |
| 404 | Client Error | Resource not found | Verify resource ID exists |
| 409 | Client Error | Resource conflict (e.g., duplicate) | Check for existing resources |
| 429 | Client Error | Rate limit exceeded | Wait and retry with backoff |
| 500 | Server Error | Internal server error | Retry with exponential backoff |
| 502/503/504 | Server Error | Service temporarily unavailable | Retry 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.
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
| Field | Type | Description | Example |
|---|
code | string | Machine-readable error code for programmatic handling | INSUFFICIENT_BALANCE |
message | string | Human-readable error description with guidance | The source account has insufficient balance... |
details | object | Additional 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 Code | HTTP Status | Description |
|---|
INVALID_PARAMETERS | 400 | Invalid request parameters |
INVALID_ACCOUNT | 400 | Invalid account identifier |
INVALID_CARD_ACCOUNT | 400 | Invalid card account identifier |
INSUFFICIENT_BALANCE | 400 | Account doesn’t have sufficient balance |
INVALID_SIGNATURE | 401 | JWT signature verification failed |
RATE_LIMIT_EXCEEDED | 429 | Too 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
- Validate JSON - Ensure request bodies are valid JSON before sending
- Use consistent formatting - Follow the API specification exactly
- Include all required fields - Check endpoint documentation
- Use proper data types - Numbers as numbers, not strings (unless specified)
Error Handling
- Check status codes first - Handle different status codes appropriately
- Use error codes, not messages - Parse the
code field for programmatic handling
- Log request IDs - Always capture
x-request-id for debugging
- Implement retry logic - For transient failures (5xx, 429)
- Don’t retry client errors - Fix 4xx errors before retrying
- Use idempotency keys - Prevents duplicate operations
- Implement connection pooling - Reuse HTTP connections
- Batch operations - When possible, use batch endpoints
- Respect rate limits - Monitor your usage and implement backoff
Security
- Never log sensitive data - Don’t log full request/response bodies
- Store credentials securely - Use environment variables or secure vaults
- Rotate keys regularly - Follow security best practices
- 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