Sign in

Getting started

You can be sending your first report in about five minutes.

  1. Sign in with Google to create your account.
  2. Create a project and choose your platform.
  3. Copy your Project ID and publishable key.
  4. Add the SDK or call the REST API.
  5. Send a test report.
  6. See it in your dashboard inbox.

Authentication

Two kinds of project keys exist with completely different permissions.

KeyHeaderUse it for
pk_live_…X-Project-KeySubmit feedback. Safe to embed in apps and websites.
sk_live_…Authorization: BearerRead project data from your backend. Never ship it to a client.

Create a project

Each project has an internal ID (never exposed) and a public Project ID (prj_…). A publishable key is generated automatically.

API keys

Create, name, rotate and revoke keys from the Integrate tab. Secret keys are shown once and only their hash is stored. Publishable keys can be rotated with no downtime.

REST API

All endpoints are versioned under /v1, use JSON over HTTPS (UTF-8) and return a consistent success/error envelope.

POST /v1/feedbackSubmit a report (publishable key)
POST /v1/projects/{project_id}/feedbackSubmit a report (publishable key)
POST /v1/feedback/{id}/attachmentsUpload an attachment (token from submit response)
GET /v1/configPublic SDK configuration (publishable key)
GET /v1/projects/{project_id}Project details (secret key, server only)
GET /v1/projects/{project_id}/feedbackList reports, cursor-paginated (secret key, server only)
curl -X POST https://relay-wishkit.reza-rzny.workers.dev/v1/feedback \
  -H "Content-Type: application/json" \
  -H "X-Project-Key: pk_live_YOUR_PUBLISHABLE_KEY" \
  -d '{
    "type": "bug",
    "title": "Login button does not work",
    "message": "Nothing happens when I tap the login button.",
    "test": true,
    "context": { "platform": "ios", "app_version": "4.2.0", "os": "iOS", "os_version": "26.0", "locale": "en-US" },
    "metadata": { "screen": "login" }
  }'

# 201 → { "success": true, "data": { "id": "fbk_…", "status": "new", "created_at": "…" } }
// Response envelope — always the same shape
{ "success": true,  "data": { } }
{ "success": false, "error": { "code": "PROJECT_QUOTA_EXCEEDED", "message": "…", "request_id": "…" } }

// Request fields (unknown fields are rejected)
type         "bug" | "feature_request" | "feedback" | "issue"   (default "feedback")
message      string, 1–5000 chars            (required)
title        string, ≤160 chars
user         { id?: string ≤128, name?: string ≤80, email?: string }
installation_id   "install_…" random, resettable
context      { platform, app_version, build, os, os_version, device, device_type, locale,
               language, timezone, sdk_version, browser, browser_version, viewport{width,height}, page_url }
metadata     ≤20 keys; key [A-Za-z0-9_.-]{1,40}; value string ≤200 | number | boolean; ≤4 KB total
test         boolean — marks the report as test data

Swift / SwiftUI

The Swift examples call the same REST API as every other language.

Official Swift Package

Install RelaySDK 1.3.0+ with Swift Package Manager. It includes a unified tabbed UI for feedback submission, developer replies, public board, voting and changelog.

github.com/reza-rezaeian/RelaySDK
// Step 1 — Xcode ▸ File ▸ Add Package Dependencies
// https://github.com/reza-rezaeian/RelaySDK.git (version 1.3.0+)
// Step 2 — Configure once at launch
import SwiftUI
import RelaySDK

@main
struct MyApp: App {
    init() {
        Relay.configure(
            projectKey: "pk_live_YOUR_PUBLISHABLE_KEY",
            baseURL: URL(string: "https://relay-wishkit.reza-rzny.workers.dev")!
        )
    }
    var body: some Scene { WindowGroup { ContentView() } }
}

// Step 3 — Show the complete customer center.
// Feedback opens first; Messages, Public Board and What's New appear at the top.
struct ContentView: View {
    @State private var showFeedback = false
    var body: some View {
        Button("Send feedback") { showFeedback = true }
            .sheet(isPresented: $showFeedback) { RelayFeedbackCenterView() }
    }
}

// Optional direct submission
Task { try await Relay.submit(type: .feedback, message: "Hello from SwiftUI") }
import RelaySDK

Relay.configure(projectKey: "pk_live_YOUR_PUBLISHABLE_KEY", baseURL: URL(string: "https://relay-wishkit.reza-rzny.workers.dev")!)

// Submit directly
Task {
    try await Relay.submit(type: .bug, title: "Login button does not work",
                                 message: "Nothing happens when I tap login.")
}

let board = try await Relay.board()
let changelog = try await Relay.changelog()
let messages = try await Relay.refreshAllThreads()
try await Relay.setVote(feedbackId: board.items[0].id, voted: true)

// Ready-made SwiftUI views:
// RelayFeedbackCenterView, RelayFeedbackView, RelayMyMessagesView,
// RelayBoardView and RelayChangelogView

JavaScript

The JavaScript examples call the same REST API as every other language.

<script src="https://relay-wishkit.reza-rzny.workers.dev/sdk/feedbackkit.js"></script>
<script>
  FeedbackKit.init({ projectKey: "pk_live_YOUR_PUBLISHABLE_KEY" /*, locale: "nl" */ });

  // Open the built-in dialog (en, ar, fa, nl, fr, es — RTL aware)
  document.querySelector("#feedback-btn").addEventListener("click", () => FeedbackKit.open());

  // …or submit directly
  FeedbackKit.submit({ type: "bug", message: "Checkout button is not responding." });
</script>

Kotlin

// Dependency-free example using HttpURLConnection (run off the main thread).
import java.net.HttpURLConnection
import java.net.URL

fun sendFeedback(message: String) {
    val body = """{"type":"bug","message":"""$message""","context":{"platform":"android","sdk_version":"rest-1"}}"""
    val c = URL("https://relay-wishkit.reza-rzny.workers.dev/v1/feedback").openConnection() as HttpURLConnection
    c.requestMethod = "POST"
    c.setRequestProperty("Content-Type", "application/json")
    c.setRequestProperty("X-Project-Key", "pk_live_YOUR_PUBLISHABLE_KEY")
    c.doOutput = true
    c.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) }
    check(c.responseCode == 201) { "Feedback failed: ${c.responseCode}" }
}

Flutter

// pubspec.yaml → dependencies: http: ^1.2.0
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> sendFeedback(String message) async {
  final res = await http.post(
    Uri.parse('https://relay-wishkit.reza-rzny.workers.dev/v1/feedback'),
    headers: {'Content-Type': 'application/json', 'X-Project-Key': 'pk_live_YOUR_PUBLISHABLE_KEY'},
    body: jsonEncode({'type': 'feedback', 'message': message, 'context': {'platform': 'flutter'}}),
  );
  if (res.statusCode != 201) throw Exception('Feedback failed: ${res.statusCode}');
}

Python

# Server-side example. Reading feedback needs a SECRET key — keep it in an environment variable.
import os, requests

r = requests.post("https://relay-wishkit.reza-rzny.workers.dev/v1/feedback",
    headers={"X-Project-Key": "pk_live_YOUR_PUBLISHABLE_KEY"},
    json={"type": "bug", "message": "Something went wrong.", "context": {"platform": "backend"}},
    timeout=10)
r.raise_for_status()

# List reports (secret key, server only)
res = requests.get("https://relay-wishkit.reza-rzny.workers.dev/v1/projects/prj_YOUR_PROJECT_ID/feedback?status=new",
    headers={"Authorization": "Bearer " + os.environ["SUGKIT_SECRET_KEY"]}, timeout=10)

PHP

<?php
$ch = curl_init("https://relay-wishkit.reza-rzny.workers.dev/v1/feedback");
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ["Content-Type: application/json", "X-Project-Key: pk_live_YOUR_PUBLISHABLE_KEY"],
  CURLOPT_POSTFIELDS => json_encode(["type" => "bug", "message" => "Something went wrong."]),
]);
$response = curl_exec($ch);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 201) { error_log("Feedback failed"); }

C#

using System.Net.Http.Json;

var http = new HttpClient { BaseAddress = new Uri("https://relay-wishkit.reza-rzny.workers.dev") };
http.DefaultRequestHeaders.Add("X-Project-Key", "pk_live_YOUR_PUBLISHABLE_KEY");

var res = await http.PostAsJsonAsync("/v1/feedback", new {
    type = "bug",
    message = "Something went wrong.",
    context = new { platform = "dotnet" }
});
res.EnsureSuccessStatusCode(); // 201 Created

Errors

Errors always contain a machine-readable code. Never parse the message.

AUTH_REQUIRED401
ACCESS_DENIED403
CSRF_FAILED403
ORIGIN_NOT_ALLOWED403
ACCOUNT_SUSPENDED403
VALIDATION_FAILED400
INVALID_JSON400
PAYLOAD_TOO_LARGE413
UNSUPPORTED_MEDIA_TYPE415
NOT_FOUND404
PROJECT_NOT_FOUND404
FEEDBACK_NOT_FOUND404
PROJECT_LIMIT_REACHED403
PROJECT_QUOTA_EXCEEDED429
STORAGE_QUOTA_EXCEEDED403
INVALID_API_KEY401
INSUFFICIENT_SCOPE403
SECRET_KEY_IN_BROWSER403
RATE_LIMITED429
PLAN_REQUIRED402
BILLING_UNAVAILABLE503
TURNSTILE_FAILED403
FEATURE_DISABLED403
INVALID_UPLOAD_TOKEN401
UNSUPPORTED_FILE_TYPE415
KEY_LIMIT_REACHED403
METHOD_NOT_ALLOWED405
INTERNAL_ERROR500

Rate limits

Limits apply per IP, API key and project. When exceeded you receive HTTP 429 with a Retry-After header. Back off and retry.

HTTP/1.1 429 Too Many Requests
Retry-After: 42

{ "success": false, "error": { "code": "RATE_LIMITED", "message": "…" } }

Security best practices

  • Never embed secret keys (sk_…) in apps, websites or public repositories.
  • Treat publishable keys as public: rely on quotas, allowed origins and Turnstile, not secrecy.
  • Identify users with your own opaque ID instead of names or emails.
  • Rotate keys if you suspect misuse; revoked keys stop working immediately.
  • Validate webhooks and never log API keys.
  • Use allowed origins for websites; native apps have no trustworthy Origin.

Privacy & data collection

SDKs collect only what is needed to debug. You control every category per project and the server discards anything disabled.

FieldWhy it is collected
platform, os, os_versionReproduce platform-specific bugs.
app_version, buildKnow which release a bug affects.
device, device_typeHardware-specific issues (off by default).
locale, language, timezoneLocalisation and date/time issues.
sdk_versionDiagnose SDK versions.
browser, viewport, page_url (no query string)Reproduce web issues. Query strings and fragments are removed.
user.id / name / email (only if YOU send them)Only if your app sends them; optional.
installation_id (random, resettable)Group reports from one installation without personal data.

We never collect contacts, precise location, advertising identifiers, passwords, tokens, cookies, clipboard contents or files. Screenshots are only sent when the end user chooses to attach them.

Reports are kept according to your plan's retention period. You can delete a report, a project or your account data at any time.