Kaushal's Photo
Home Follow on LinkedIn Share on LinkedIn Share on Twitter

REST API Design

Designing a REST API for a payment service requires careful planning to ensure security, scalability, and usability. Here are comprehensive guidelines to follow:


1. General REST API Design Principles

  • Stateless: Ensure the API is stateless, where each request contains all necessary information.
  • Resource-Oriented: Use nouns to represent resources (/payments, /transactions, /customers) and HTTP methods for actions.
  • Versioning: Version your APIs using the URL or headers (e.g., /v1/payments).
  • Consistent Naming: Use snake_case or kebab-case for naming resources (/payment-methods, /transaction-history).

2. Endpoint Structure

Use clear, hierarchical endpoint paths to represent resources:

GET /payments                     # Retrieve all payments
POST /payments                    # Create a new payment
GET /payments/{paymentId}         # Retrieve a specific payment
PUT /payments/{paymentId}         # Update a specific payment
DELETE /payments/{paymentId}      # Cancel a payment

Additional examples:

  • Payment Methods:
    GET /payment-methods            # List all supported payment methods
    POST /payment-methods           # Add a new payment method for a user
  • Transactions:
    GET /transactions               # List all transactions
    GET /transactions/{transactionId} # Get transaction details

3. HTTP Methods

Use appropriate HTTP methods for operations:

  • GET: Retrieve resources (e.g., transaction history, payment details).
  • POST: Create new resources (e.g., initiate a payment, add a payment method).
  • PUT: Update existing resources (e.g., modify payment details).
  • PATCH: Partially update resources (e.g., update payment status).
  • DELETE: Remove or cancel resources (e.g., cancel a pending payment).

4. Request and Response Structure

Request Structure:

  1. Use JSON: Accept JSON as the standard payload format.
  2. Validation: Validate all incoming data for required fields, data types, and formats.
  3. Idempotency: Ensure POST and PUT operations are idempotent, especially for payment initiation.
    • Example: Use an idempotency_key for duplicate request detection.

Example Payment Initiation Request:

{
  "amount": 100.00,
  "currency": "USD",
  "payment_method": "card",
  "payment_method_details": {
    "card_number": "4111111111111111",
    "expiry_date": "12/26",
    "cvv": "123"
  },
  "description": "Payment for Order #12345"
}

Response Structure:

  1. HTTP Status Codes:
    • 200 OK: Request succeeded.
    • 201 Created: Resource created (e.g., payment initialized).
    • 400 Bad Request: Invalid input.
    • 401 Unauthorized: Authentication failed.
    • 404 Not Found: Resource not found.
    • 500 Internal Server Error: Unexpected server error.
  2. Use Standard Fields: Include:
    • status: HTTP status message.
    • message: Additional information about the response.
    • data: Response payload for successful requests.
    • errors: Detailed error information for failed requests.

Example Response for Payment Creation:

{
  "status": "success",
  "data": {
    "payment_id": "pay_1234567890",
    "amount": 100.00,
    "currency": "USD",
    "status": "pending",
    "created_at": "2024-11-28T12:34:56Z"
  }
}

5. Authentication and Authorization

  1. OAuth2/Bearer Tokens: Use OAuth2 or JWT for user authentication.
    Authorization: Bearer {token}
  2. Role-Based Access Control (RBAC): Implement RBAC to restrict access (e.g., customers vs. admins).
  3. Secure Endpoints: Protect sensitive endpoints with SSL/TLS.

6. Security

  1. PCI DSS Compliance: If processing credit card payments, ensure PCI DSS compliance.
    • Avoid storing sensitive cardholder information like CVV.
  2. Tokenization: Use tokens for card details to reduce exposure of sensitive data.
    {
      "payment_method_token": "tok_1234567890"
    }
  3. Rate Limiting: Prevent abuse by limiting the number of API requests per client.
  4. Idempotency: Handle duplicate requests with idempotency keys.
  5. Validation: Validate all user input to prevent injection attacks.

7. Error Handling

Provide clear, consistent error messages:

  • Error Response Example:
    {
      "status": "error",
      "message": "Invalid card number",
      "errors": [
        {
          "field": "card_number",
          "message": "Card number must be 16 digits"
        }
      ]
    }
  • Include error codes for easier debugging (e.g., ERR_CARD_INVALID, ERR_PAYMENT_FAILED).

8. Pagination and Filtering

For endpoints returning large datasets (e.g., transaction history), use:

  1. Pagination:
    GET /transactions?page=1&limit=10
    Response:
    {
      "data": [...],
      "pagination": {
        "page": 1,
        "limit": 10,
        "total_pages": 5,
        "total_items": 50
      }
    }
  2. Filtering:
    GET /transactions?status=success&date_from=2024-01-01

9. Webhooks

  1. Event Notifications: Provide webhook support to notify clients of events like payment success or failure.
    {
      "event": "payment.success",
      "data": {
        "payment_id": "pay_1234567890",
        "status": "success",
        "amount": 100.00,
        "currency": "USD"
      }
    }
  2. Security:
    • Use a shared secret to validate webhook requests.
    • Retry failed webhook deliveries.

10. Testing and Documentation

  1. Automated Tests:
    • Unit tests for individual endpoints.
    • Integration tests for payment workflows.
  2. API Documentation: Use tools like Swagger/OpenAPI or Postman to generate API documentation. Example tools:

Example Workflow for Payment API

  1. User Initiates a Payment:

    • POST /payments
      {
        "amount": 100.00,
        "currency": "USD",
        "payment_method_token": "tok_1234567890"
      }
  2. Payment Service Processes the Payment:

    • Payment status changes to processing, then success or failure.
  3. User Queries Payment Status:

    • GET /payments/{paymentId}
      {
        "payment_id": "pay_1234567890",
        "status": "success",
        "amount": 100.00,
        "currency": "USD"
      }
  4. Webhook Notification for Payment Success:

    • Webhook event: payment.success.

Webhooks are a powerful mechanism for sending event-driven notifications from a server to client systems. In the context of payment services, webhooks notify clients about important events like successful payments, failed transactions, or refund completions. Below is a detailed overview of webhook events and how to implement them effectively.


Common Webhook Events in a Payment Service

Here’s a list of typical webhook events for a payment API:

1. Payment Events

  • payment.created: A new payment is initiated.
  • payment.processing: Payment is being processed.
  • payment.success: Payment is successfully completed.
  • payment.failed: Payment failed due to an issue (e.g., insufficient funds, invalid payment method).

2. Refund Events

  • refund.initiated: A refund request has been initiated.
  • refund.processing: Refund is being processed.
  • refund.completed: Refund has been successfully processed.
  • refund.failed: Refund failed (e.g., due to technical issues or invalid request).

3. Dispute Events

  • dispute.created: A dispute has been raised against a transaction.
  • dispute.resolved: The dispute has been resolved.
  • dispute.closed: The dispute is closed without a resolution.

4. Subscription Events (if applicable)

  • subscription.created: A new subscription is created.
  • subscription.updated: Subscription details have been updated.
  • subscription.canceled: A subscription is canceled.
  • subscription.renewed: A subscription is successfully renewed.

5. Other Events

  • payment_method.added: A new payment method is added.
  • payment_method.updated: An existing payment method is updated.
  • payment_method.deleted: A payment method is removed.
  • customer.created: A new customer profile is created.

Webhook Payload Structure

A webhook payload is typically a JSON object containing event details.

Example Payload for payment.success:

{
  "event": "payment.success",
  "data": {
    "payment_id": "pay_1234567890",
    "amount": 100.00,
    "currency": "USD",
    "status": "success",
    "created_at": "2024-11-28T12:34:56Z",
    "customer_id": "cus_1234567890",
    "metadata": {
      "order_id": "order_9876543210"
    }
  }
}

Example Payload for refund.completed:

{
  "event": "refund.completed",
  "data": {
    "refund_id": "ref_9876543210",
    "payment_id": "pay_1234567890",
    "amount": 50.00,
    "currency": "USD",
    "status": "completed",
    "processed_at": "2024-11-28T13:45:12Z"
  }
}

Webhook Implementation Best Practices

1. Secure Webhooks

  1. Shared Secret: Include a shared secret to validate webhook requests. Compute an HMAC using the secret and the payload, and send it in the header:

    X-Signature: sha256=generated_hmac_signature

    Example HMAC calculation:

    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    
    public static String calculateHMAC(String payload, String secret) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
            mac.init(secretKeySpec);
            byte[] hmacBytes = mac.doFinal(payload.getBytes());
            StringBuilder result = new StringBuilder();
            for (byte b : hmacBytes) {
                result.append(String.format("%02x", b));
            }
            return result.toString();
        } catch (Exception e) {
            throw new RuntimeException("Error calculating HMAC", e);
        }
    }
  2. TLS/HTTPS: Always deliver webhooks over HTTPS to ensure payload security.

  3. IP Whitelisting: Optionally, allow only webhook requests from specific IP addresses.

2. Retry Mechanism

  • Implement retries for failed webhook deliveries.
  • Exponential backoff: Retry with increasing intervals (e.g., 5s, 15s, 30s).
  • Set a maximum retry limit (e.g., 10 retries).

3. Idempotency

  • Include a unique event_id in each webhook to ensure the same event isn't processed multiple times.
  • Clients should store processed event IDs and ignore duplicates.

4. Event Logging

  • Log all webhook events for debugging and traceability.
  • Include request and response details.

5. Subscription Management

  • Allow clients to manage webhook subscriptions via API:
    • Subscribe to specific events (POST /webhooks).
    • Update webhook URL (PUT /webhooks/{id}).
    • Unsubscribe (DELETE /webhooks/{id}).

Webhook Subscription Example

Endpoint to Subscribe to Webhooks:

Request:

POST /webhooks
Content-Type: application/json
Authorization: Bearer {token}

Payload:

{
  "url": "https://example.com/webhook",
  "events": ["payment.success", "payment.failed"],
  "secret": "my_shared_secret"
}

Response:

{
  "webhook_id": "whk_1234567890",
  "status": "active",
  "url": "https://example.com/webhook",
  "events": ["payment.success", "payment.failed"]
}

Endpoint to List Active Webhooks:

Request:

GET /webhooks
Authorization: Bearer {token}

Response:

[
  {
    "webhook_id": "whk_1234567890",
    "status": "active",
    "url": "https://example.com/webhook",
    "events": ["payment.success", "payment.failed"]
  }
]

Testing Webhooks

Use tools like Webhook.site or Postman to test your webhook implementation. They allow you to inspect incoming webhook requests and verify payloads, headers, and delivery.


Challenges to Address

  1. Event Ordering: Webhooks might not arrive in order. Include timestamps to let clients reorder events.
  2. Delivery Latency: Ensure webhooks are delivered promptly, but be prepared for occasional delays.
  3. Retry Storms: Design retry mechanisms to avoid overwhelming client systems with retries.

Webhook Lifecycle

  1. Event Occurs: A payment is processed or refunded.
  2. Webhook Triggered: The system sends a POST request to the subscribed webhook URL.
  3. Client Acknowledgment:
    • Respond with a 200 OK status to confirm successful delivery.
    • Non-200 responses trigger retries.
  4. Retries: The server retries failed webhook deliveries.

Implementing webhooks in a Spring Boot application involves creating endpoints to receive events and handling those events securely and efficiently. Here's a step-by-step guide:


1. Set Up the Webhook Endpoint

Create a REST controller with a POST endpoint to handle incoming webhook payloads.

Example:

@RestController
@RequestMapping("/webhooks")
public class WebhookController {

    @PostMapping
    public ResponseEntity<String> handleWebhook(@RequestBody Map<String, Object> payload, 
                                                @RequestHeader Map<String, String> headers) {
        // Log the payload for debugging (remove this in production)
        System.out.println("Webhook payload: " + payload);

        // Verify the webhook signature for security (explained later)
        String signature = headers.get("X-Signature");
        if (!verifySignature(payload, signature)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid signature");
        }

        // Process the webhook event
        String eventType = (String) payload.get("event");
        switch (eventType) {
            case "payment.success":
                handlePaymentSuccess(payload);
                break;
            case "payment.failed":
                handlePaymentFailed(payload);
                break;
            default:
                System.out.println("Unhandled event type: " + eventType);
        }

        // Return a 200 OK response to acknowledge receipt
        return ResponseEntity.ok("Webhook received successfully");
    }

    private void handlePaymentSuccess(Map<String, Object> payload) {
        System.out.println("Processing payment success event...");
        // Add your custom logic here
    }

    private void handlePaymentFailed(Map<String, Object> payload) {
        System.out.println("Processing payment failed event...");
        // Add your custom logic here
    }

    private boolean verifySignature(Map<String, Object> payload, String signature) {
        // Implement signature verification logic here
        return true; // Placeholder: Replace with actual verification
    }
}

2. Secure the Webhook Endpoint

a. Validate the Signature

Use HMAC (e.g., HmacSHA256) to validate the signature. This ensures that the webhook request originates from the trusted server.

Example Signature Verification:

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class WebhookSignatureVerifier {

    private static final String SECRET = "your_shared_secret";

    public static boolean verifySignature(Map<String, Object> payload, String signature) {
        try {
            // Convert payload to JSON string
            String payloadJson = new ObjectMapper().writeValueAsString(payload);

            // Generate HMAC SHA256 signature
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET.getBytes(), "HmacSHA256");
            mac.init(secretKeySpec);
            byte[] hmacBytes = mac.doFinal(payloadJson.getBytes());

            // Compare generated signature with the received signature
            String expectedSignature = Base64.getEncoder().encodeToString(hmacBytes);
            return expectedSignature.equals(signature);
        } catch (Exception e) {
            throw new RuntimeException("Error verifying signature", e);
        }
    }
}

Integrate this method into the WebhookController.


b. Use HTTPS

Ensure that your webhook endpoint is accessible only over HTTPS to prevent man-in-the-middle attacks.


3. Handle Idempotency

To prevent duplicate event processing (e.g., due to retries), ensure idempotency by storing event IDs in a database.

Example:

@Service
public class IdempotencyService {
    private final Set<String> processedEventIds = ConcurrentHashMap.newKeySet();

    public boolean isAlreadyProcessed(String eventId) {
        return !processedEventIds.add(eventId);
    }
}

// Integrate into WebhookController
@Autowired
private IdempotencyService idempotencyService;

@PostMapping
public ResponseEntity<String> handleWebhook(@RequestBody Map<String, Object> payload) {
    String eventId = (String) payload.get("event_id");
    if (idempotencyService.isAlreadyProcessed(eventId)) {
        return ResponseEntity.status(HttpStatus.CONFLICT).body("Duplicate event");
    }

    // Process event here...
    return ResponseEntity.ok("Webhook processed");
}

4. Respond Appropriately

  • Always respond with 200 OK for successful webhook handling.
  • Respond with appropriate error codes (400, 401, etc.) for failed validation or unprocessable requests.

5. Testing Webhooks

Local Testing:

  1. Use ngrok to expose your local server to the internet:
    ngrok http 8080
  2. Set the ngrok URL (https://your-ngrok-url/webhooks) as the webhook endpoint.

Simulating Webhook Requests:

Use tools like Postman or curl to simulate webhook POST requests.

curl -X POST https://your-ngrok-url/webhooks \
-H "Content-Type: application/json" \
-H "X-Signature: your_signature" \
-d '{"event": "payment.success", "data": {"amount": 100}}'

6. Logging and Monitoring

  • Use a logging framework like SLF4J or Logback to log incoming requests and processing details.
  • Consider integrating monitoring tools like Prometheus, Datadog, or Splunk for webhook analytics.

7. Retry Logic (Optional)

If your webhook fails, the sender may retry. Implement a mechanism to handle retries gracefully:

  • Check for duplicate event IDs.
  • Log retry attempts for debugging.

8. Webhook Management (Optional)

If you want your application to manage webhook subscriptions (e.g., create, update, delete), create additional endpoints:

Example Endpoints:

  • Register a Webhook URL
    @PostMapping("/register")
    public ResponseEntity<String> registerWebhook(@RequestBody WebhookRegistrationRequest request) {
        // Save the webhook URL in your database
        return ResponseEntity.ok("Webhook registered");
    }
  • List Active Webhooks
    @GetMapping
    public ResponseEntity<List<Webhook>> listWebhooks() {
        // Return list of registered webhooks
        return ResponseEntity.ok(webhookService.getAllWebhooks());
    }

This implementation provides a robust, secure, and extensible way to handle webhooks in a Spring Boot application. Let me know if you’d like more detailed code or explanations!

© 2025 Kaushal Kishor. All rights reserved.

Follow on LinkedIn