---
title: "Feedback and support"
description: "Let users report a problem or open a ticket, with the trace, version and steps attached and screenshots redacted."
section: "interfaces"
status: "Building for 0.6 · ships as a beta API"
spec: "debug-loop"
canonical: "https://mainsdk.dev/docs/interfaces/feedback"
updated: "2026-10-28"
---

# Feedback and support

> Let users report a problem or open a ticket, with the trace, version and steps attached and screenshots redacted.

Status: Building for 0.6 · ships as a beta API · Canonical: https://mainsdk.dev/docs/interfaces/feedback · Updated: 2026-10-28

Let your users report a problem or open a ticket. Each report arrives with the trace, version, device and steps, a redacted screenshot, and its matching error. Use it for a report button.

> **Note:** Built in 0.6 as part of the debug loop (spec `debug-loop`) and covered by tests against the in-repo Cloud. Screenshots are uploaded through getqr.id through a port you give it; the pixel redaction is built in. Forwarding to Linear, Jira, Zendesk and Plain is a port the host fills in.

## At a glance

- Status: Building for 0.6 · ships as a beta API
- Phase: 2 (Cloud and Teams, 1.0)
- Milestone: 0.6 Cloud
- Plan: free in your app (Apache-2.0)
- Spec: https://github.com/hubxero/mainsdk/blob/develop/docs/specs/debug-loop.md
- Category: Messaging and notifications

Calls:

- `feedback.open({ category? })`
- `feedback.onCrash({ code? })`
- `feedback.onFailedAction({ code })`
- `support.ticket({ subject, body })`

In main.yaml:

```yaml
feedback:
  tracker: linear
  prompt: { onCrash: true, onFailedAction: true }
```

Guarantees, whatever the driver:

- Screenshots redacted
- Support access audited


## Drivers

Pick one in main.yaml with `feedback.driver`.

| Driver | Id in main.yaml | Default | Note |
| --- | --- | --- | --- |
| Built in | `builtin` | yes |  |
| Intercom | `intercom` |  |  |
| Zendesk | `zendesk` |  |  |
| Plain | `plain` |  |  |

## Codes

No check or runtime codes are assigned to this interface yet.

## Features

| Feature | Phase | Milestone | Spec ticket |
| --- | --- | --- | --- |
| Report capture with trace, version and steps | 2 | 0.6 | `debug-loop#feedback-capture` |
| Support tickets | 2 | 0.6 |  |

## MainSDK Cloud

Everything on this page is free in your app.
Cloud adds:

| Plan | What it adds |
| --- | --- |
| teams | Inbox history and triage of reports |


## Quick start

```yaml title="main.yaml"
feedback:
  tracker: linear
  prompt: { onCrash: true, onFailedAction: true }
```

```ts title="app/feedback.ts"
import { createFeedback, StepRecorder } from "mainsdk/feedback";

// Stand-ins for what your host app provides.
declare const currentTrace: () => string | undefined;
declare const showReportForm: (o: { reason: string }) => Promise<{ text: string } | undefined>;
declare const getqr: { upload: (bytes: Uint8Array, o: { name: string; type: string }) => Promise<{ url: string }> };
declare const cloud: { post: (path: string, body: unknown) => Promise<void> };

export const steps = new StepRecorder();

export const { feedback, support } = createFeedback({
  version: "3.1.0",
  device: () => ({ platform: "ios", os: "19.1" }),
  steps,
  trace: () => currentTrace(),
  ask: (o) => showReportForm(o),            // returns { text, screenshot? } or undefined
  upload: (bytes, o) => getqr.upload(bytes, o), // a getqr.id upload; returns { url }
  send: (report) => cloud.post("/v1/feedback", report),
  prompt: { onCrash: true, onFailedAction: true },
});
```

## Configuration

```yaml title="main.yaml"
feedback:
  tracker: linear                # linear | jira | zendesk | plain
  prompt: { onCrash: true, onFailedAction: true }
```

## What you get

| Call | What it does |
| --- | --- |
| `feedback.open({ category? })` | Shows the report form and sends the report. |
| `feedback.onCrash({ code? })` | Asks after a crash, only when `prompt.onCrash` is on. |
| `feedback.onFailedAction({ code })` | Asks after a failed action, only when `prompt.onFailedAction` is on. |
| `support.ticket({ subject, body })` | Sends a ticket with the same trace, version and steps. |

Each call returns the report id, or `undefined` when the customer closed the form or anything failed. A failure never reaches your app (ADR-0003).

## Examples

### What a report carries

A report holds the trace, the app version, the device, the last steps and, when there is one, a screenshot. Text is redacted for emails, bearer tokens and key formats. Note each step with `steps.note("opened checkout")`; never put a value from user data in a step.

### A screenshot with its text fields blacked out

Give the screenshot as RGBA pixels with the boxes of its text fields. They are filled black before the image is uploaded, and only the redacted image ever leaves the device. The Cloud accepts only a getqr.id link as the screenshot.

```ts
import { maskScreenshot } from "mainsdk/feedback";

const width = 2;
const height = 2;
const rgba = new Uint8Array(width * height * 4);

const safe = maskScreenshot({ width, height, rgba, fields: [{ x: 40, y: 120, w: 280, h: 44 }] });
```

### Read the inbox

The console inbox groups each report with the error it matches, by trace first and then by error code, so one error and the customers who hit it sit together. Free keeps the free history window; Teams keeps it all and can forward a report to your tracker.

### Reply to the reporter

Replies go back to the customer through [`notify`](https://mainsdk.dev/docs/interfaces/notify).

```ts title="lib/reply.ts"
import { notify } from "mainsdk";

export async function replyToReport(user: { id: string }, reportId: string) {
  await notify(user, { template: "feedback.reply", data: { reportId }, category: "support" });
}
```

## Errors and checks

| Code | When | Fix |
| --- | --- | --- |
| `MAIN-DEBUG-0007` | The Cloud refused a report: a field is missing, or the screenshot is not a getqr.id upload | Send `id`, `text`, `version`, `device` and `steps`, and upload the screenshot through getqr.id |
| `MAIN-DEBUG-0001` | Forwarding a report to a tracker needs Teams | Move the project to Teams |

## Swapping drivers

```yaml title="main.yaml"
feedback:
  tracker: zendesk   # was linear
```

Your `feedback.open()` and `support.ticket()` calls stay the same. Past tickets stay in the old tool.

## Testing

`createFeedback` takes every port as an argument, so a test passes a fake `ask`, `upload` and `send` and reads what was sent. See [Debug your app](https://mainsdk.dev/docs/guides/debugging) and [Test your app](https://mainsdk.dev/docs/guides/testing).

## Security and privacy

- Screenshots are redacted before they leave the device.
- Support access to a user's data is audited.
- Reports carry the trace, so you see what failed without asking the user for details.

### What Cloud adds

Capturing reports is free. Teams adds inbox history and triage of reports. See [Free in-app and Cloud](https://mainsdk.dev/docs/concepts/free-and-cloud).

## FAQ

**Can reports land in my issue tracker?** Teams can forward a report to Linear, Jira, Zendesk or Plain through a forwarder your host supplies.

**Why a screenshot link and not the image?** Screenshots are uploaded through getqr.id and the Cloud stores only the link (ADR-0009). See [Debugging](https://mainsdk.dev/docs/guides/debugging).

## Next

- [Logs, audit and telemetry](https://mainsdk.dev/docs/interfaces/log)
- [Notifications](https://mainsdk.dev/docs/interfaces/notify)
- [Health and status page](https://mainsdk.dev/docs/interfaces/status-page)
