API Documentation

Integrate Feedback Pulse into your application and start collecting user feedback in minutes.

Quick Start: Create a project in the Projects page to get your API key, then use it in the X-API-Key header for all requests.

Authentication

All API requests must include your project's API key in the X-API-Key header.

Your API key is automatically generated when you create a project. Go to the Projects page, create a new project, and your API key will be displayed. Each project has its own unique API key.

Example Header

X-API-Key: fp_your_api_key_here

Security Tip: Never expose your API key in client-side code for web applications. Use a backend proxy or environment variables for mobile apps.

Base URL

All API endpoints are relative to the following base URL:

https://api.fpulse.app/v1

For local development: http://localhost:5052/api/v1

Rate Limits

API rate limits vary by subscription tier:

Plan Rate Limit Monthly Feedback
Free 100 requests/minute 1,000
Pro 1,000 requests/minute 50,000
Enterprise 10,000 requests/minute Unlimited

When rate limited, the API returns 429 Too Many Requests with a retry_after value.

GET /health

Check if the API is operational. No authentication required.

Request

curl https://api.fpulse.app/v1/health

Response

{
  "status": "ok",
  "service": "Feedback Pulse API"
}

POST /feedback

Submit user feedback from your application. This is the primary endpoint you'll use.

Request Headers

Header Value Required
X-API-Key Your project API key Required
Content-Type application/json Required

Request Body Parameters

Parameter Type Required Description
sentiment string Required "positive" or "negative"
app_version string Required Your app version (e.g., "2.1.0"). Max 50 chars.
app_type string Required "ios", "android", "web", "desktop", or "other"
comment string Optional User's feedback text. Max 5,000 chars.
screen_id string Optional Screen/page identifier (e.g., "checkout", "settings"). Max 100 chars.
user_identifier string Optional User email or ID for follow-up. Max 255 chars.
environment string Optional "development" or "production" (default)
device_info object Optional Device details object (see below)
metadata object Optional Custom key-value pairs for additional context

device_info Object

Field Type Example
os string "iOS", "Android", "Windows", "macOS"
os_version string "17.2", "14", "11"
device_model string "iPhone 15 Pro", "Pixel 8"
locale string "en_US", "de_DE"
timezone string "America/New_York", "Europe/London"

Complete Request Example

POST /api/v1/feedback HTTP/1.1
Host: api.fpulse.app
Content-Type: application/json
X-API-Key: fp_your_api_key_here

{
  "sentiment": "positive",
  "comment": "Love the new dark mode feature! Makes it much easier to use at night.",
  "app_version": "2.1.0",
  "app_type": "ios",
  "screen_id": "settings",
  "user_identifier": "user@example.com",
  "environment": "production",
  "device_info": {
    "os": "iOS",
    "os_version": "17.2",
    "device_model": "iPhone 15 Pro",
    "locale": "en_US",
    "timezone": "America/New_York"
  },
  "metadata": {
    "feature_used": "dark_mode",
    "session_duration": 300,
    "is_premium_user": true
  }
}

Response

Success (201 Created)

{
  "success": true,
  "feedback_id": 12345,
  "message": "Feedback submitted successfully"
}

Validation Error (400 Bad Request)

{
  "success": false,
  "error": "Validation failed",
  "code": "VALIDATION_ERROR",
  "details": [
    "sentiment is required",
    "app_type must be one of: ios, android, web, desktop, other"
  ]
}

Authentication Error (401 Unauthorized)

{
  "success": false,
  "error": "Invalid or missing API key",
  "code": "UNAUTHORIZED"
}

Limit Exceeded (403 Forbidden)

{
  "success": false,
  "error": "Monthly feedback limit exceeded. Please upgrade your plan.",
  "code": "LIMIT_EXCEEDED"
}

POST /feedback/batch

Submit multiple feedback items in a single request. Useful for syncing offline feedback or bulk imports. Maximum 100 items per batch.

Request Body

{
  "feedbacks": [
    {
      "sentiment": "positive",
      "comment": "Great app!",
      "app_version": "2.1.0",
      "app_type": "ios",
      "screen_id": "home"
    },
    {
      "sentiment": "negative",
      "comment": "App crashed on checkout",
      "app_version": "2.1.0",
      "app_type": "android",
      "screen_id": "checkout"
    }
  ]
}

Response

Full Success (201 Created)

{
  "success": true,
  "total": 2,
  "succeeded": 2,
  "failed": 0,
  "results": [
    { "index": 0, "success": true, "feedback_id": 12345 },
    { "index": 1, "success": true, "feedback_id": 12346 }
  ]
}

Partial Success (207 Multi-Status)

{
  "success": false,
  "total": 2,
  "succeeded": 1,
  "failed": 1,
  "results": [
    { "index": 0, "success": true, "feedback_id": 12345 },
    { "index": 1, "success": false, "errors": ["invalid sentiment"] }
  ]
}

GET /project/info

Retrieve information about your project. Useful for verifying your API key is valid and checking your subscription tier.

Request

curl -H "X-API-Key: fp_your_api_key" https://api.fpulse.app/v1/project/info

Response

{
  "success": true,
  "project": {
    "id": 1,
    "name": "My Mobile App"
  },
  "organization": {
    "name": "My Company",
    "tier": "pro"
  }
}

JavaScript / TypeScript

Use this example for web applications, React, Vue, Node.js, or any JavaScript environment.

Installation

# No package needed - use fetch API
# Or install axios if preferred:
npm install axios

Basic Example (Fetch)

async function submitFeedback(sentiment, comment) {
  const response = await fetch('https://api.fpulse.app/v1/feedback', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': 'fp_your_api_key_here'
    },
    body: JSON.stringify({
      sentiment: sentiment,  // 'positive' or 'negative'
      comment: comment,
      app_version: '2.1.0',
      app_type: 'web',
      screen_id: window.location.pathname,
      environment: process.env.NODE_ENV === 'production' ? 'production' : 'development'
    })
  });

  const data = await response.json();

  if (data.success) {
    console.log('Feedback submitted:', data.feedback_id);
  } else {
    console.error('Error:', data.error);
  }

  return data;
}

// Usage
submitFeedback('positive', 'Great new feature!');

React Component Example

import { useState } from 'react';

const API_KEY = process.env.REACT_APP_FPULSE_API_KEY;
const API_URL = 'https://api.fpulse.app/v1/feedback';

function FeedbackWidget({ screenId }) {
  const [comment, setComment] = useState('');
  const [submitted, setSubmitted] = useState(false);
  const [loading, setLoading] = useState(false);

  const submitFeedback = async (sentiment) => {
    setLoading(true);

    try {
      const response = await fetch(API_URL, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': API_KEY
        },
        body: JSON.stringify({
          sentiment,
          comment,
          app_version: process.env.REACT_APP_VERSION || '1.0.0',
          app_type: 'web',
          screen_id: screenId,
          environment: process.env.NODE_ENV
        })
      });

      const data = await response.json();

      if (data.success) {
        setSubmitted(true);
      }
    } catch (error) {
      console.error('Feedback error:', error);
    } finally {
      setLoading(false);
    }
  };

  if (submitted) {
    return <div>Thank you for your feedback!</div>;
  }

  return (
    <div className="feedback-widget">
      <p>How was your experience?</p>
      <textarea
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        placeholder="Tell us more (optional)"
      />
      <div>
        <button onClick={() => submitFeedback('positive')} disabled={loading}>
          Good
        </button>
        <button onClick={() => submitFeedback('negative')} disabled={loading}>
          Bad
        </button>
      </div>
    </div>
  );
}

Swift (iOS)

Native Swift implementation for iOS, iPadOS, and macOS applications.

FeedbackPulse.swift

import Foundation
import UIKit

class FeedbackPulse {
    static let shared = FeedbackPulse()

    private let apiKey: String
    private let baseURL = "https://api.fpulse.app/v1"
    private let appVersion: String
    private let environment: String

    init(apiKey: String = "fp_your_api_key_here",
         appVersion: String = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
         environment: String = "production") {
        self.apiKey = apiKey
        self.appVersion = appVersion
        self.environment = environment
    }

    struct FeedbackResult: Codable {
        let success: Bool
        let feedbackId: Int?
        let error: String?

        enum CodingKeys: String, CodingKey {
            case success
            case feedbackId = "feedback_id"
            case error
        }
    }

    func submit(
        sentiment: String,  // "positive" or "negative"
        comment: String? = nil,
        screenId: String? = nil,
        userIdentifier: String? = nil,
        metadata: [String: Any]? = nil,
        completion: @escaping (Result<FeedbackResult, Error>) -> Void
    ) {
        guard let url = URL(string: "\(baseURL)/feedback") else {
            completion(.failure(NSError(domain: "FeedbackPulse", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])))
            return
        }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")

        var body: [String: Any] = [
            "sentiment": sentiment,
            "app_version": appVersion,
            "app_type": "ios",
            "environment": environment,
            "device_info": [
                "os": "iOS",
                "os_version": UIDevice.current.systemVersion,
                "device_model": UIDevice.current.model,
                "locale": Locale.current.identifier,
                "timezone": TimeZone.current.identifier
            ]
        ]

        if let comment = comment { body["comment"] = comment }
        if let screenId = screenId { body["screen_id"] = screenId }
        if let userIdentifier = userIdentifier { body["user_identifier"] = userIdentifier }
        if let metadata = metadata { body["metadata"] = metadata }

        request.httpBody = try? JSONSerialization.data(withJSONObject: body)

        URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }

            guard let data = data else {
                completion(.failure(NSError(domain: "FeedbackPulse", code: -2, userInfo: [NSLocalizedDescriptionKey: "No data"])))
                return
            }

            do {
                let result = try JSONDecoder().decode(FeedbackResult.self, from: data)
                completion(.success(result))
            } catch {
                completion(.failure(error))
            }
        }.resume()
    }
}

// Usage Example
FeedbackPulse.shared.submit(
    sentiment: "positive",
    comment: "Love the new update!",
    screenId: "settings",
    userIdentifier: "user@example.com"
) { result in
    switch result {
    case .success(let response):
        print("Feedback submitted: \(response.feedbackId ?? 0)")
    case .failure(let error):
        print("Error: \(error.localizedDescription)")
    }
}

Kotlin (Android)

Native Kotlin implementation for Android applications using coroutines.

FeedbackPulse.kt

import android.content.Context
import android.os.Build
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
import java.util.*

class FeedbackPulse(
    private val apiKey: String,
    private val context: Context,
    private val environment: String = "production"
) {
    companion object {
        private const val BASE_URL = "https://api.fpulse.app/v1"
    }

    data class FeedbackResult(
        val success: Boolean,
        val feedbackId: Int? = null,
        val error: String? = null
    )

    suspend fun submit(
        sentiment: String,
        comment: String? = null,
        screenId: String? = null,
        userIdentifier: String? = null,
        metadata: Map<String, Any>? = null
    ): FeedbackResult = withContext(Dispatchers.IO) {
        try {
            val url = URL("$BASE_URL/feedback")
            val connection = url.openConnection() as HttpURLConnection

            connection.requestMethod = "POST"
            connection.setRequestProperty("Content-Type", "application/json")
            connection.setRequestProperty("X-API-Key", apiKey)
            connection.doOutput = true

            val appVersion = context.packageManager
                .getPackageInfo(context.packageName, 0).versionName

            val deviceInfo = JSONObject().apply {
                put("os", "Android")
                put("os_version", Build.VERSION.RELEASE)
                put("device_model", "${Build.MANUFACTURER} ${Build.MODEL}")
                put("locale", Locale.getDefault().toString())
                put("timezone", TimeZone.getDefault().id)
            }

            val body = JSONObject().apply {
                put("sentiment", sentiment)
                put("app_version", appVersion)
                put("app_type", "android")
                put("environment", environment)
                put("device_info", deviceInfo)
                comment?.let { put("comment", it) }
                screenId?.let { put("screen_id", it) }
                userIdentifier?.let { put("user_identifier", it) }
                metadata?.let { put("metadata", JSONObject(it)) }
            }

            connection.outputStream.bufferedWriter().use {
                it.write(body.toString())
            }

            val responseCode = connection.responseCode
            val response = if (responseCode == HttpURLConnection.HTTP_CREATED) {
                connection.inputStream.bufferedReader().readText()
            } else {
                connection.errorStream.bufferedReader().readText()
            }

            val json = JSONObject(response)
            FeedbackResult(
                success = json.getBoolean("success"),
                feedbackId = if (json.has("feedback_id")) json.getInt("feedback_id") else null,
                error = if (json.has("error")) json.getString("error") else null
            )
        } catch (e: Exception) {
            FeedbackResult(success = false, error = e.message)
        }
    }
}

// Usage Example (in a ViewModel or Activity)
class MyViewModel : ViewModel() {
    private val feedbackPulse = FeedbackPulse(
        apiKey = "fp_your_api_key_here",
        context = applicationContext
    )

    fun submitFeedback(isPositive: Boolean, comment: String?) {
        viewModelScope.launch {
            val result = feedbackPulse.submit(
                sentiment = if (isPositive) "positive" else "negative",
                comment = comment,
                screenId = "main_screen"
            )

            if (result.success) {
                Log.d("Feedback", "Submitted: ${result.feedbackId}")
            } else {
                Log.e("Feedback", "Error: ${result.error}")
            }
        }
    }
}

Dart / Flutter

Cross-platform Flutter implementation for iOS, Android, Web, and Desktop applications.

pubspec.yaml

dependencies:
  http: ^1.1.0

lib/feedback_pulse.dart

import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';

class FeedbackResult {
  final bool success;
  final int? feedbackId;
  final String? error;

  FeedbackResult({required this.success, this.feedbackId, this.error});

  factory FeedbackResult.fromJson(Map<String, dynamic> json) {
    return FeedbackResult(
      success: json['success'] ?? false,
      feedbackId: json['feedback_id'],
      error: json['error'],
    );
  }
}

class FeedbackPulse {
  static const String _baseUrl = 'https://api.fpulse.app/v1';

  final String apiKey;
  final String appVersion;
  final String appType;
  final String environment;

  FeedbackPulse({
    required this.apiKey,
    required this.appVersion,
    this.appType = 'ios',
    this.environment = 'production',
  });

  Map<String, String> get _deviceInfo {
    return {
      'os': Platform.operatingSystem,
      'os_version': Platform.operatingSystemVersion,
      'locale': Platform.localeName,
    };
  }

  Future<FeedbackResult> submit({
    required String sentiment,
    String? comment,
    String? screenId,
    String? userIdentifier,
    Map<String, dynamic>? metadata,
  }) async {
    try {
      final body = {
        'sentiment': sentiment,
        'app_version': appVersion,
        'app_type': appType,
        'environment': environment,
        'device_info': _deviceInfo,
        if (comment != null) 'comment': comment,
        if (screenId != null) 'screen_id': screenId,
        if (userIdentifier != null) 'user_identifier': userIdentifier,
        if (metadata != null) 'metadata': metadata,
      };

      final response = await http.post(
        Uri.parse('$_baseUrl/feedback'),
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': apiKey,
        },
        body: jsonEncode(body),
      );

      final data = jsonDecode(response.body) as Map<String, dynamic>;
      return FeedbackResult.fromJson(data);
    } catch (e) {
      return FeedbackResult(success: false, error: e.toString());
    }
  }
}

// Usage Example
// final client = FeedbackPulse(
//   apiKey: 'fp_your_api_key_here',
//   appVersion: '2.1.0',
//   appType: Platform.isIOS ? 'ios' : 'android',
//   environment: kReleaseMode ? 'production' : 'development',
// );
//
// final result = await client.submit(
//   sentiment: 'positive',
//   comment: 'Flutter is amazing!',
//   screenId: 'home_screen',
// );
//
// if (result.success) {
//   print('Feedback submitted: ${result.feedbackId}');
// } else {
//   print('Error: ${result.error}');
// }

Flutter Widget Example

import 'package:flutter/material.dart';

class FeedbackWidget extends StatefulWidget {
  final FeedbackPulse client;
  final String screenId;

  const FeedbackWidget({
    super.key,
    required this.client,
    required this.screenId,
  });

  @override
  State<FeedbackWidget> createState() => _FeedbackWidgetState();
}

class _FeedbackWidgetState extends State<FeedbackWidget> {
  final _commentController = TextEditingController();
  bool _submitted = false;
  bool _loading = false;

  Future<void> _submit(String sentiment) async {
    setState(() => _loading = true);

    final result = await widget.client.submit(
      sentiment: sentiment,
      comment: _commentController.text.isNotEmpty
          ? _commentController.text
          : null,
      screenId: widget.screenId,
    );

    setState(() {
      _loading = false;
      _submitted = result.success;
    });

    if (!result.success && mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Error: ${result.error}')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_submitted) {
      return const Card(
        child: Padding(
          padding: EdgeInsets.all(16),
          child: Text('Thank you for your feedback!'),
        ),
      );
    }

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('How was your experience?'),
            const SizedBox(height: 8),
            TextField(
              controller: _commentController,
              decoration: const InputDecoration(
                hintText: 'Tell us more (optional)',
                border: OutlineInputBorder(),
              ),
              maxLines: 3,
            ),
            const SizedBox(height: 16),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton.icon(
                  onPressed: _loading ? null : () => _submit('positive'),
                  icon: const Icon(Icons.thumb_up),
                  label: const Text('Good'),
                ),
                ElevatedButton.icon(
                  onPressed: _loading ? null : () => _submit('negative'),
                  icon: const Icon(Icons.thumb_down),
                  label: const Text('Bad'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.red,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Python

Python implementation for backend services, scripts, or desktop applications.

Installation

pip install requests

feedback_pulse.py

import requests
import platform
from typing import Optional, Dict, Any
from dataclasses import dataclass


@dataclass
class FeedbackResult:
    success: bool
    feedback_id: Optional[int] = None
    error: Optional[str] = None


class FeedbackPulse:
    BASE_URL = "https://api.fpulse.app/v1"

    def __init__(
        self,
        api_key: str,
        app_version: str = "1.0.0",
        app_type: str = "desktop",
        environment: str = "production"
    ):
        self.api_key = api_key
        self.app_version = app_version
        self.app_type = app_type
        self.environment = environment
        self.session = requests.Session()
        self.session.headers.update({
            "Content-Type": "application/json",
            "X-API-Key": api_key
        })

    def _get_device_info(self) -> Dict[str, str]:
        return {
            "os": platform.system(),
            "os_version": platform.release(),
            "device_model": platform.machine(),
            "locale": "en_US",  # You might want to detect this
        }

    def submit(
        self,
        sentiment: str,  # "positive" or "negative"
        comment: Optional[str] = None,
        screen_id: Optional[str] = None,
        user_identifier: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None
    ) -> FeedbackResult:
        """Submit feedback to Feedback Pulse API."""

        payload = {
            "sentiment": sentiment,
            "app_version": self.app_version,
            "app_type": self.app_type,
            "environment": self.environment,
            "device_info": self._get_device_info()
        }

        if comment:
            payload["comment"] = comment
        if screen_id:
            payload["screen_id"] = screen_id
        if user_identifier:
            payload["user_identifier"] = user_identifier
        if metadata:
            payload["metadata"] = metadata

        try:
            response = self.session.post(
                f"{self.BASE_URL}/feedback",
                json=payload
            )
            data = response.json()

            return FeedbackResult(
                success=data.get("success", False),
                feedback_id=data.get("feedback_id"),
                error=data.get("error")
            )
        except Exception as e:
            return FeedbackResult(success=False, error=str(e))

    def submit_batch(
        self,
        feedbacks: list[Dict[str, Any]]
    ) -> Dict[str, Any]:
        """Submit multiple feedback items at once."""

        try:
            response = self.session.post(
                f"{self.BASE_URL}/feedback/batch",
                json={"feedbacks": feedbacks}
            )
            return response.json()
        except Exception as e:
            return {"success": False, "error": str(e)}


# Usage Example
if __name__ == "__main__":
    # Initialize the client
    client = FeedbackPulse(
        api_key="fp_your_api_key_here",
        app_version="2.1.0",
        app_type="desktop"
    )

    # Submit positive feedback
    result = client.submit(
        sentiment="positive",
        comment="The new export feature is amazing!",
        screen_id="export_dialog",
        user_identifier="user@example.com",
        metadata={
            "export_format": "csv",
            "record_count": 1500
        }
    )

    if result.success:
        print(f"Feedback submitted! ID: {result.feedback_id}")
    else:
        print(f"Error: {result.error}")

Java

Java implementation for backend services, Android (if not using Kotlin), and enterprise applications.

Maven Dependency

<!-- Using java.net.http (Java 11+) - no external dependencies needed -->
<!-- Or add Gson for JSON parsing: -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.10.1</version>
</dependency>

FeedbackPulse.java

import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

public class FeedbackPulse {
    private static final String BASE_URL = "https://api.fpulse.app/v1";
    private final HttpClient httpClient;
    private final Gson gson;
    private final String apiKey;
    private final String appVersion;
    private final String appType;
    private final String environment;

    public FeedbackPulse(String apiKey, String appVersion) {
        this(apiKey, appVersion, "desktop", "production");
    }

    public FeedbackPulse(String apiKey, String appVersion, String appType, String environment) {
        this.apiKey = apiKey;
        this.appVersion = appVersion;
        this.appType = appType;
        this.environment = environment;
        this.httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .build();
        this.gson = new Gson();
    }

    public record FeedbackResult(
        boolean success,
        @SerializedName("feedback_id") Integer feedbackId,
        String error
    ) {}

    public FeedbackResult submit(String sentiment, String comment, String screenId) {
        return submit(sentiment, comment, screenId, null, null);
    }

    public FeedbackResult submit(
        String sentiment,
        String comment,
        String screenId,
        String userIdentifier,
        Map<String, Object> metadata
    ) {
        try {
            Map<String, Object> body = new HashMap<>();
            body.put("sentiment", sentiment);
            body.put("app_version", appVersion);
            body.put("app_type", appType);
            body.put("environment", environment);
            body.put("device_info", getDeviceInfo());

            if (comment != null) body.put("comment", comment);
            if (screenId != null) body.put("screen_id", screenId);
            if (userIdentifier != null) body.put("user_identifier", userIdentifier);
            if (metadata != null) body.put("metadata", metadata);

            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(BASE_URL + "/feedback"))
                .header("Content-Type", "application/json")
                .header("X-API-Key", apiKey)
                .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(body)))
                .build();

            HttpResponse<String> response = httpClient.send(
                request,
                HttpResponse.BodyHandlers.ofString()
            );

            return gson.fromJson(response.body(), FeedbackResult.class);
        } catch (Exception e) {
            return new FeedbackResult(false, null, e.getMessage());
        }
    }

    private Map<String, String> getDeviceInfo() {
        Map<String, String> info = new HashMap<>();
        info.put("os", System.getProperty("os.name"));
        info.put("os_version", System.getProperty("os.version"));
        info.put("java_version", System.getProperty("java.version"));
        info.put("locale", java.util.Locale.getDefault().toString());
        return info;
    }
}

// Usage Example
// FeedbackPulse client = new FeedbackPulse(
//     "fp_your_api_key_here",
//     "2.1.0",
//     "desktop",
//     "production"
// );
//
// FeedbackPulse.FeedbackResult result = client.submit(
//     "positive",
//     "Java integration works great!",
//     "main_screen"
// );
//
// if (result.success()) {
//     System.out.println("Feedback submitted: " + result.feedbackId());
// } else {
//     System.err.println("Error: " + result.error());
// }

Spring Boot Integration

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Service
public class FeedbackService {
    private final WebClient webClient;
    private final String appVersion;

    public FeedbackService(
        @Value("${fpulse.api-key}") String apiKey,
        @Value("${fpulse.app-version:1.0.0}") String appVersion
    ) {
        this.appVersion = appVersion;
        this.webClient = WebClient.builder()
            .baseUrl("https://api.fpulse.app/v1")
            .defaultHeader("X-API-Key", apiKey)
            .defaultHeader("Content-Type", "application/json")
            .build();
    }

    public Mono<FeedbackResult> submit(String sentiment, String comment, String screenId) {
        var body = Map.of(
            "sentiment", sentiment,
            "comment", comment != null ? comment : "",
            "screen_id", screenId,
            "app_version", appVersion,
            "app_type", "web",
            "environment", "production"
        );

        return webClient.post()
            .uri("/feedback")
            .bodyValue(body)
            .retrieve()
            .bodyToMono(FeedbackResult.class);
    }

    public record FeedbackResult(boolean success, Integer feedbackId, String error) {}
}

PHP 8+

Modern PHP implementation using native features. Works with Laravel, Symfony, or vanilla PHP.

FeedbackPulse.php

<?php

declare(strict_types=1);

readonly class FeedbackResult
{
    public function __construct(
        public bool $success,
        public ?int $feedbackId = null,
        public ?string $error = null
    ) {}
}

class FeedbackPulse
{
    private const BASE_URL = 'https://api.fpulse.app/v1';

    public function __construct(
        private string $apiKey,
        private string $appVersion = '1.0.0',
        private string $appType = 'web',
        private string $environment = 'production'
    ) {}

    public function submit(
        string $sentiment,
        ?string $comment = null,
        ?string $screenId = null,
        ?string $userIdentifier = null,
        ?array $metadata = null
    ): FeedbackResult {
        $payload = [
            'sentiment' => $sentiment,
            'app_version' => $this->appVersion,
            'app_type' => $this->appType,
            'environment' => $this->environment,
            'device_info' => [
                'os' => PHP_OS_FAMILY,
                'os_version' => php_uname('r'),
                'locale' => locale_get_default() ?: 'en_US',
            ],
        ];

        if ($comment !== null) $payload['comment'] = $comment;
        if ($screenId !== null) $payload['screen_id'] = $screenId;
        if ($userIdentifier !== null) $payload['user_identifier'] = $userIdentifier;
        if ($metadata !== null) $payload['metadata'] = $metadata;

        try {
            $response = $this->request('POST', '/feedback', $payload);

            return new FeedbackResult(
                success: $response['success'] ?? false,
                feedbackId: $response['feedback_id'] ?? null,
                error: $response['error'] ?? null
            );
        } catch (Exception $e) {
            return new FeedbackResult(success: false, error: $e->getMessage());
        }
    }

    public function submitBatch(array $feedbacks): array
    {
        return $this->request('POST', '/feedback/batch', ['feedbacks' => $feedbacks]);
    }

    private function request(string $method, string $endpoint, array $data): array
    {
        $ch = curl_init(self::BASE_URL . $endpoint);

        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'X-API-Key: ' . $this->apiKey,
            ],
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_POSTFIELDS => json_encode($data),
            CURLOPT_TIMEOUT => 30,
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($response === false) {
            throw new Exception('Request failed');
        }

        return json_decode($response, true) ?? [];
    }
}

// Usage Example
$client = new FeedbackPulse(
    apiKey: 'fp_your_api_key_here',
    appVersion: '2.1.0',
    appType: 'web'
);

$result = $client->submit(
    sentiment: 'positive',
    comment: 'The checkout process is so smooth!',
    screenId: 'checkout',
    userIdentifier: 'user@example.com',
    metadata: ['cart_total' => 99.99]
);

if ($result->success) {
    echo "Feedback submitted! ID: {$result->feedbackId}\n";
} else {
    echo "Error: {$result->error}\n";
}

Laravel Integration

// In a Controller or Service
use Illuminate\Support\Facades\Http;

class FeedbackService
{
    public function submit(string $sentiment, ?string $comment = null): array
    {
        $response = Http::withHeaders([
            'X-API-Key' => config('services.fpulse.api_key'),
        ])->post('https://api.fpulse.app/v1/feedback', [
            'sentiment' => $sentiment,
            'comment' => $comment,
            'app_version' => config('app.version'),
            'app_type' => 'web',
            'screen_id' => request()->path(),
            'user_identifier' => auth()->user()?->email,
            'environment' => app()->environment(),
        ]);

        return $response->json();
    }
}

Go

Idiomatic Go implementation for backend services and CLI tools.

feedbackpulse.go

package feedbackpulse

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"runtime"
	"time"
)

const baseURL = "https://api.fpulse.app/v1"

type Client struct {
	APIKey      string
	AppVersion  string
	AppType     string
	Environment string
	HTTPClient  *http.Client
}

type FeedbackRequest struct {
	Sentiment      string                 `json:"sentiment"`
	Comment        string                 `json:"comment,omitempty"`
	AppVersion     string                 `json:"app_version"`
	AppType        string                 `json:"app_type"`
	ScreenID       string                 `json:"screen_id,omitempty"`
	UserIdentifier string                 `json:"user_identifier,omitempty"`
	Environment    string                 `json:"environment"`
	DeviceInfo     map[string]string      `json:"device_info,omitempty"`
	Metadata       map[string]interface{} `json:"metadata,omitempty"`
}

type FeedbackResult struct {
	Success    bool   `json:"success"`
	FeedbackID int    `json:"feedback_id,omitempty"`
	Error      string `json:"error,omitempty"`
}

func NewClient(apiKey, appVersion string) *Client {
	return &Client{
		APIKey:      apiKey,
		AppVersion:  appVersion,
		AppType:     "desktop",
		Environment: "production",
		HTTPClient: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

func (c *Client) Submit(sentiment, comment, screenID string) (*FeedbackResult, error) {
	req := FeedbackRequest{
		Sentiment:   sentiment,
		Comment:     comment,
		AppVersion:  c.AppVersion,
		AppType:     c.AppType,
		ScreenID:    screenID,
		Environment: c.Environment,
		DeviceInfo: map[string]string{
			"os":         runtime.GOOS,
			"os_version": runtime.Version(),
			"arch":       runtime.GOARCH,
		},
	}

	return c.doRequest("POST", "/feedback", req)
}

func (c *Client) SubmitWithMetadata(
	sentiment, comment, screenID, userID string,
	metadata map[string]interface{},
) (*FeedbackResult, error) {
	req := FeedbackRequest{
		Sentiment:      sentiment,
		Comment:        comment,
		AppVersion:     c.AppVersion,
		AppType:        c.AppType,
		ScreenID:       screenID,
		UserIdentifier: userID,
		Environment:    c.Environment,
		Metadata:       metadata,
		DeviceInfo: map[string]string{
			"os":         runtime.GOOS,
			"os_version": runtime.Version(),
			"arch":       runtime.GOARCH,
		},
	}

	return c.doRequest("POST", "/feedback", req)
}

func (c *Client) doRequest(method, endpoint string, body interface{}) (*FeedbackResult, error) {
	jsonData, err := json.Marshal(body)
	if err != nil {
		return nil, fmt.Errorf("marshal error: %w", err)
	}

	req, err := http.NewRequest(method, baseURL+endpoint, bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("request error: %w", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-API-Key", c.APIKey)

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("http error: %w", err)
	}
	defer resp.Body.Close()

	var result FeedbackResult
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("decode error: %w", err)
	}

	return &result, nil
}

// Usage Example
// func main() {
//     client := feedbackpulse.NewClient("fp_your_api_key", "2.1.0")
//     client.Environment = "production"
//
//     result, err := client.Submit("positive", "Great CLI tool!", "main")
//     if err != nil {
//         log.Fatal(err)
//     }
//
//     if result.Success {
//         fmt.Printf("Feedback submitted! ID: %d\n", result.FeedbackID)
//     }
// }

Rust

Type-safe Rust implementation using reqwest and serde.

Cargo.toml

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }

src/feedback_pulse.rs

use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

const BASE_URL: &str = "https://api.fpulse.app/v1";

#[derive(Debug, Serialize)]
pub struct FeedbackRequest {
    pub sentiment: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub comment: Option<String>,
    pub app_version: String,
    pub app_type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub screen_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user_identifier: Option<String>,
    pub environment: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub device_info: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
}

#[derive(Debug, Deserialize)]
pub struct FeedbackResult {
    pub success: bool,
    pub feedback_id: Option<i32>,
    pub error: Option<String>,
}

pub struct FeedbackPulse {
    client: Client,
    api_key: String,
    app_version: String,
    app_type: String,
    environment: String,
}

impl FeedbackPulse {
    pub fn new(api_key: &str, app_version: &str) -> Self {
        Self {
            client: Client::new(),
            api_key: api_key.to_string(),
            app_version: app_version.to_string(),
            app_type: "desktop".to_string(),
            environment: "production".to_string(),
        }
    }

    pub fn with_environment(mut self, env: &str) -> Self {
        self.environment = env.to_string();
        self
    }

    pub fn with_app_type(mut self, app_type: &str) -> Self {
        self.app_type = app_type.to_string();
        self
    }

    fn get_device_info() -> HashMap<String, String> {
        let mut info = HashMap::new();
        info.insert("os".to_string(), std::env::consts::OS.to_string());
        info.insert("arch".to_string(), std::env::consts::ARCH.to_string());
        info
    }

    pub async fn submit(
        &self,
        sentiment: &str,
        comment: Option<&str>,
        screen_id: Option<&str>,
    ) -> Result<FeedbackResult, reqwest::Error> {
        let request = FeedbackRequest {
            sentiment: sentiment.to_string(),
            comment: comment.map(|s| s.to_string()),
            app_version: self.app_version.clone(),
            app_type: self.app_type.clone(),
            screen_id: screen_id.map(|s| s.to_string()),
            user_identifier: None,
            environment: self.environment.clone(),
            device_info: Some(Self::get_device_info()),
            metadata: None,
        };

        self.client
            .post(format!("{}/feedback", BASE_URL))
            .header("Content-Type", "application/json")
            .header("X-API-Key", &self.api_key)
            .json(&request)
            .send()
            .await?
            .json()
            .await
    }
}

// Usage Example
// #[tokio::main]
// async fn main() -> Result<(), Box<dyn std::error::Error>> {
//     let client = FeedbackPulse::new("fp_your_api_key", "2.1.0")
//         .with_environment("production");
//
//     let result = client
//         .submit("positive", Some("Blazing fast!"), Some("main"))
//         .await?;
//
//     if result.success {
//         println!("Feedback submitted! ID: {:?}", result.feedback_id);
//     } else {
//         eprintln!("Error: {:?}", result.error);
//     }
//
//     Ok(())
// }

Ruby

Ruby implementation for Rails applications and scripts.

feedback_pulse.rb

require 'net/http'
require 'json'
require 'uri'

class FeedbackPulse
  BASE_URL = 'https://api.fpulse.app/v1'.freeze

  FeedbackResult = Struct.new(:success, :feedback_id, :error, keyword_init: true)

  def initialize(api_key:, app_version: '1.0.0', app_type: 'web', environment: 'production')
    @api_key = api_key
    @app_version = app_version
    @app_type = app_type
    @environment = environment
  end

  def submit(sentiment:, comment: nil, screen_id: nil, user_identifier: nil, metadata: nil)
    payload = {
      sentiment: sentiment,
      app_version: @app_version,
      app_type: @app_type,
      environment: @environment,
      device_info: device_info
    }

    payload[:comment] = comment if comment
    payload[:screen_id] = screen_id if screen_id
    payload[:user_identifier] = user_identifier if user_identifier
    payload[:metadata] = metadata if metadata

    response = request(:post, '/feedback', payload)

    FeedbackResult.new(
      success: response['success'],
      feedback_id: response['feedback_id'],
      error: response['error']
    )
  rescue StandardError => e
    FeedbackResult.new(success: false, error: e.message)
  end

  def submit_batch(feedbacks)
    request(:post, '/feedback/batch', { feedbacks: feedbacks })
  end

  private

  def device_info
    {
      os: RUBY_PLATFORM,
      os_version: RUBY_VERSION,
      locale: ENV['LANG'] || 'en_US'
    }
  end

  def request(method, endpoint, body)
    uri = URI("#{BASE_URL}#{endpoint}")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    request = case method
              when :post then Net::HTTP::Post.new(uri)
              when :get then Net::HTTP::Get.new(uri)
              end

    request['Content-Type'] = 'application/json'
    request['X-API-Key'] = @api_key
    request.body = body.to_json

    response = http.request(request)
    JSON.parse(response.body)
  end
end

# Usage Example
client = FeedbackPulse.new(
  api_key: 'fp_your_api_key_here',
  app_version: '2.1.0',
  app_type: 'web'
)

result = client.submit(
  sentiment: 'positive',
  comment: 'Ruby rocks!',
  screen_id: 'dashboard',
  user_identifier: 'user@example.com'
)

if result.success
  puts "Feedback submitted! ID: #{result.feedback_id}"
else
  puts "Error: #{result.error}"
end

Rails Integration

# config/initializers/feedback_pulse.rb
FEEDBACK_PULSE = FeedbackPulse.new(
  api_key: Rails.application.credentials.fpulse_api_key,
  app_version: Rails.application.config.version,
  environment: Rails.env
)

# In a controller
class FeedbackController < ApplicationController
  def create
    result = FEEDBACK_PULSE.submit(
      sentiment: params[:sentiment],
      comment: params[:comment],
      screen_id: request.path,
      user_identifier: current_user&.email
    )

    if result.success
      render json: { success: true, id: result.feedback_id }
    else
      render json: { success: false, error: result.error }, status: :unprocessable_entity
    end
  end
end

C# / .NET

Modern C# implementation for .NET 6+ applications, ASP.NET Core, MAUI, and Unity.

FeedbackPulse.cs

using System.Net.Http.Json;
using System.Text.Json.Serialization;

namespace FeedbackPulse;

public record FeedbackResult(
    [property: JsonPropertyName("success")] bool Success,
    [property: JsonPropertyName("feedback_id")] int? FeedbackId = null,
    [property: JsonPropertyName("error")] string? Error = null
);

public class FeedbackRequest
{
    [JsonPropertyName("sentiment")] public required string Sentiment { get; init; }
    [JsonPropertyName("comment")] public string? Comment { get; init; }
    [JsonPropertyName("app_version")] public required string AppVersion { get; init; }
    [JsonPropertyName("app_type")] public required string AppType { get; init; }
    [JsonPropertyName("screen_id")] public string? ScreenId { get; init; }
    [JsonPropertyName("user_identifier")] public string? UserIdentifier { get; init; }
    [JsonPropertyName("environment")] public string Environment { get; init; } = "production";
    [JsonPropertyName("device_info")] public Dictionary<string, string>? DeviceInfo { get; init; }
    [JsonPropertyName("metadata")] public Dictionary<string, object>? Metadata { get; init; }
}

public class FeedbackPulseClient : IDisposable
{
    private const string BaseUrl = "https://api.fpulse.app/v1";
    private readonly HttpClient _httpClient;
    private readonly string _appVersion;
    private readonly string _appType;
    private readonly string _environment;

    public FeedbackPulseClient(
        string apiKey,
        string appVersion,
        string appType = "desktop",
        string environment = "production")
    {
        _appVersion = appVersion;
        _appType = appType;
        _environment = environment;

        _httpClient = new HttpClient
        {
            BaseAddress = new Uri(BaseUrl),
            Timeout = TimeSpan.FromSeconds(30)
        };
        _httpClient.DefaultRequestHeaders.Add("X-API-Key", apiKey);
    }

    public async Task<FeedbackResult> SubmitAsync(
        string sentiment,
        string? comment = null,
        string? screenId = null,
        string? userIdentifier = null,
        Dictionary<string, object>? metadata = null,
        CancellationToken ct = default)
    {
        var request = new FeedbackRequest
        {
            Sentiment = sentiment,
            Comment = comment,
            AppVersion = _appVersion,
            AppType = _appType,
            ScreenId = screenId,
            UserIdentifier = userIdentifier,
            Environment = _environment,
            Metadata = metadata,
            DeviceInfo = new Dictionary<string, string>
            {
                ["os"] = Environment.OSVersion.Platform.ToString(),
                ["os_version"] = Environment.OSVersion.VersionString,
                ["runtime"] = Environment.Version.ToString()
            }
        };

        try
        {
            var response = await _httpClient.PostAsJsonAsync("/feedback", request, ct);
            return await response.Content.ReadFromJsonAsync<FeedbackResult>(ct)
                   ?? new FeedbackResult(false, Error: "Failed to parse response");
        }
        catch (Exception ex)
        {
            return new FeedbackResult(false, Error: ex.Message);
        }
    }

    public void Dispose() => _httpClient.Dispose();
}

// Usage Example
// var client = new FeedbackPulseClient(
//     apiKey: "fp_your_api_key_here",
//     appVersion: "2.1.0",
//     appType: "desktop"
// );
//
// var result = await client.SubmitAsync(
//     sentiment: "positive",
//     comment: ".NET is awesome!",
//     screenId: "main_window"
// );
//
// if (result.Success)
//     Console.WriteLine($"Feedback submitted! ID: {result.FeedbackId}");
// else
//     Console.WriteLine($"Error: {result.Error}");

ASP.NET Core Dependency Injection

// Program.cs
builder.Services.AddSingleton(sp =>
    new FeedbackPulseClient(
        apiKey: builder.Configuration["FeedbackPulse:ApiKey"]!,
        appVersion: Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0.0",
        appType: "web",
        environment: builder.Environment.EnvironmentName.ToLower()
    ));

// In a Controller
[ApiController]
[Route("api/[controller]")]
public class FeedbackController : ControllerBase
{
    private readonly FeedbackPulseClient _feedbackClient;

    public FeedbackController(FeedbackPulseClient feedbackClient)
        => _feedbackClient = feedbackClient;

    [HttpPost]
    public async Task<IActionResult> Submit([FromBody] FeedbackDto dto)
    {
        var result = await _feedbackClient.SubmitAsync(
            sentiment: dto.Sentiment,
            comment: dto.Comment,
            screenId: Request.Path,
            userIdentifier: User.Identity?.Name
        );

        return result.Success
            ? Ok(new { id = result.FeedbackId })
            : BadRequest(new { error = result.Error });
    }
}

Elixir

Elixir implementation for Phoenix applications and OTP services.

mix.exs Dependencies

defp deps do
  [
    {:httpoison, "~> 2.0"},
    {:jason, "~> 1.4"}
  ]
end

lib/feedback_pulse.ex

defmodule FeedbackPulse do
  @moduledoc """
  Client for the Feedback Pulse API.
  """

  @base_url "https://api.fpulse.app/v1"

  defstruct [:api_key, :app_version, :app_type, :environment]

  @type t :: %__MODULE__{
    api_key: String.t(),
    app_version: String.t(),
    app_type: String.t(),
    environment: String.t()
  }

  @type feedback_result :: {:ok, map()} | {:error, String.t()}

  @doc """
  Creates a new FeedbackPulse client.
  """
  @spec new(String.t(), String.t(), keyword()) :: t()
  def new(api_key, app_version, opts \\ []) do
    %__MODULE__{
      api_key: api_key,
      app_version: app_version,
      app_type: Keyword.get(opts, :app_type, "web"),
      environment: Keyword.get(opts, :environment, "production")
    }
  end

  @doc """
  Submits feedback to the API.
  """
  @spec submit(t(), String.t(), keyword()) :: feedback_result()
  def submit(%__MODULE__{} = client, sentiment, opts \\ []) do
    body = %{
      sentiment: sentiment,
      app_version: client.app_version,
      app_type: client.app_type,
      environment: client.environment,
      device_info: get_device_info()
    }
    |> maybe_put(:comment, Keyword.get(opts, :comment))
    |> maybe_put(:screen_id, Keyword.get(opts, :screen_id))
    |> maybe_put(:user_identifier, Keyword.get(opts, :user_identifier))
    |> maybe_put(:metadata, Keyword.get(opts, :metadata))

    headers = [
      {"Content-Type", "application/json"},
      {"X-API-Key", client.api_key}
    ]

    case HTTPoison.post("#{@base_url}/feedback", Jason.encode!(body), headers) do
      {:ok, %HTTPoison.Response{status_code: 201, body: body}} ->
        {:ok, Jason.decode!(body)}

      {:ok, %HTTPoison.Response{body: body}} ->
        case Jason.decode(body) do
          {:ok, %{"error" => error}} -> {:error, error}
          _ -> {:error, "Unknown error"}
        end

      {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, to_string(reason)}
    end
  end

  defp get_device_info do
    %{
      os: to_string(:os.type() |> elem(0)),
      os_version: :erlang.system_info(:otp_release) |> to_string(),
      elixir_version: System.version(),
      locale: System.get_env("LANG", "en_US")
    }
  end

  defp maybe_put(map, _key, nil), do: map
  defp maybe_put(map, key, value), do: Map.put(map, key, value)
end

# Usage Example
# client = FeedbackPulse.new(
#   "fp_your_api_key_here",
#   "2.1.0",
#   app_type: "web",
#   environment: "production"
# )
#
# case FeedbackPulse.submit(client, "positive",
#   comment: "Elixir is elegant!",
#   screen_id: "home",
#   user_identifier: "user@example.com"
# ) do
#   {:ok, %{"feedback_id" => id}} ->
#     IO.puts("Feedback submitted: #{id}")
#
#   {:error, reason} ->
#     IO.puts("Error: #{reason}")
# end

Phoenix Integration

# config/config.exs
config :my_app, :feedback_pulse,
  api_key: System.get_env("FPULSE_API_KEY"),
  app_version: Mix.Project.config()[:version]

# lib/my_app/feedback.ex
defmodule MyApp.Feedback do
  @config Application.compile_env(:my_app, :feedback_pulse)

  def client do
    FeedbackPulse.new(
      @config[:api_key],
      @config[:app_version],
      app_type: "web",
      environment: Mix.env() |> to_string()
    )
  end

  def submit(sentiment, opts \\ []) do
    FeedbackPulse.submit(client(), sentiment, opts)
  end
end

# In a Phoenix Controller
defmodule MyAppWeb.FeedbackController do
  use MyAppWeb, :controller

  def create(conn, %{"sentiment" => sentiment, "comment" => comment}) do
    case MyApp.Feedback.submit(sentiment,
      comment: comment,
      screen_id: conn.request_path,
      user_identifier: get_session(conn, :user_email)
    ) do
      {:ok, %{"feedback_id" => id}} ->
        json(conn, %{success: true, id: id})

      {:error, reason} ->
        conn
        |> put_status(:unprocessable_entity)
        |> json(%{success: false, error: reason})
    end
  end
end

cURL

Command-line examples for testing or shell scripts.

Submit Feedback

curl -X POST https://api.fpulse.app/v1/feedback \
  -H "Content-Type: application/json" \
  -H "X-API-Key: fp_your_api_key_here" \
  -d '{
    "sentiment": "positive",
    "comment": "Great product!",
    "app_version": "2.1.0",
    "app_type": "web",
    "screen_id": "checkout"
  }'

Health Check

curl https://api.fpulse.app/v1/health

Get Project Info

curl https://api.fpulse.app/v1/project/info \
  -H "X-API-Key: fp_your_api_key_here"

Error Handling

All API errors follow a consistent format for easy handling in your application.

Error Response Format

{
  "success": false,
  "error": "Human-readable error message",
  "code": "ERROR_CODE",
  "details": ["optional", "array", "of", "specific", "errors"]
}

Error Codes

HTTP Status Code Description
400 INVALID_BODY Request body is not valid JSON
400 VALIDATION_ERROR One or more fields failed validation
400 BATCH_TOO_LARGE Batch request exceeds 100 items
401 UNAUTHORIZED Missing or invalid API key
403 LIMIT_EXCEEDED Monthly feedback limit reached
403 SUBSCRIPTION_INACTIVE Subscription has expired
429 RATE_LIMITED Too many requests, retry after delay
500 INTERNAL_ERROR Server error, please retry

Best Practices

1. Use screen_id Consistently

Always include a screen_id to identify where feedback came from. This enables powerful analytics to identify problematic screens.

2. Set Environment Correctly

Use "development" during testing and "production" for live apps. This keeps your production analytics clean.

3. Handle Errors Gracefully

Never let a feedback submission failure crash your app. Always wrap API calls in try-catch and fail silently if needed.

4. Use Batch for Offline Sync

For mobile apps, queue feedback locally when offline and use the batch endpoint to sync when connectivity returns.

5. Include Relevant Metadata

Add context using the metadata field - features used, session duration, user tier, etc. This helps correlate feedback with user behavior.

6. Protect Your API Key

For web apps, proxy API calls through your backend. For mobile apps, use secure storage and consider API key rotation.

Development vs Production

Feedback Pulse supports separate environments to keep your development testing separate from real user feedback.

Development

  • Use for testing and QA
  • Doesn't affect production metrics
  • Visible in dashboard with "Dev" filter
  • Set: "environment": "development"

Production

  • Real user feedback
  • Used for analytics and reports
  • Default if not specified
  • Set: "environment": "production"

Tip: Toggle between Dev and Prod views in your dashboard sidebar to analyze feedback from each environment separately.

Need Help?

Can't find what you're looking for? We're here to help!

How's your experience?

Thank you!

Your feedback helps us improve.

We value your privacy

We use cookies to enhance your browsing experience, analyze site traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies. Contact us