In-App Integration¶
This guide covers two ways to collect feedback from your users and route it to your Canviq board. Choose the approach that fits your UX:
| Approach | Best for | Effort |
|---|---|---|
| Embedded web form | Full board experience — voting, roadmap, auth | ~1 hour |
| API submission | Native in-app form, full control over UX | ~2 hours |
Prerequisites¶
- Your Canviq org slug — find it in your board URL:
https://{your-slug}.canviq.app/en - For API use: an org API key with
submissions:writescope — generate one at Settings → API Keys
Option 1: Embedded Web Form¶
Embed your Canviq board directly in your app. Users get the full board experience — submitting ideas, voting, viewing the roadmap — without leaving your app.
This requires no backend code for basic use.
iOS (SFSafariViewController)¶
import SafariServices
let ORG_SLUG = "your-slug" // replace with your board slug
// Open the full feedback board
func openFeedbackBoard() {
let url = URL(string: "https://\(ORG_SLUG).canviq.app/en")!
let safari = SFSafariViewController(url: url)
present(safari, animated: true)
}
// Open with the submit form pre-focused
func openSubmitFeedback() {
let url = URL(string: "https://\(ORG_SLUG).canviq.app/en/submit")!
let safari = SFSafariViewController(url: url)
present(safari, animated: true)
}
// Open the public roadmap
func openRoadmap() {
let url = URL(string: "https://\(ORG_SLUG).canviq.app/en/roadmap")!
let safari = SFSafariViewController(url: url)
present(safari, animated: true)
}
!!! important "Use SFSafariViewController, not WKWebView" WKWebView does not share the Safari cookie jar. Users will not be able to sign in or have their session persist. Always use SFSafariViewController.
Android (Chrome Custom Tabs)¶
import androidx.browser.customtabs.CustomTabsIntent
val ORG_SLUG = "your-slug" // replace with your board slug
// Open the full feedback board
fun openFeedbackBoard() {
val url = Uri.parse("https://$ORG_SLUG.canviq.app/en")
val intent = CustomTabsIntent.Builder().build()
intent.launchUrl(this, url)
}
// Open with the submit form pre-focused
fun openSubmitFeedback() {
val url = Uri.parse("https://$ORG_SLUG.canviq.app/en/submit")
CustomTabsIntent.Builder().build().launchUrl(this, url)
}
// Open the public roadmap
fun openRoadmap() {
val url = Uri.parse("https://$ORG_SLUG.canviq.app/en/roadmap")
CustomTabsIntent.Builder().build().launchUrl(this, url)
}
Web (iframe)¶
<!-- Full board embed -->
<iframe
src="https://{your-slug}.canviq.app/en"
style="width: 100%; height: 600px; border: none;"
title="Product feedback"
loading="lazy"
/>
<!-- Submit form pre-opened -->
<iframe
src="https://{your-slug}.canviq.app/en/submit"
style="width: 100%; height: 600px; border: none;"
title="Submit feedback"
/>
If your app uses a Content Security Policy, add *.canviq.app to your frame-src directive:
Deep Link Views¶
Control which screen opens by navigating to the appropriate path:
| URL | Opens |
|---|---|
https://{your-slug}.canviq.app/en | Default board (submission list) |
https://{your-slug}.canviq.app/en/submit | Submission form |
https://{your-slug}.canviq.app/en/roadmap | Public roadmap |
https://{your-slug}.canviq.app/en/submissions/{id} | Specific submission detail |
Authentication in the Embedded Board¶
Users authenticate via magic link. Canviq sends a one-time login link to their email. Once clicked, the session persists across future launches (cookies are shared with the system browser).
!!! info "Pre-authentication (coming soon)" Authenticated embed tokens will allow you to pre-authenticate your users so they arrive at the board already signed in. Contact [email protected] if this is blocking your integration.
Option 2: API Submission¶
Build a native feedback form in your app and send submissions to Canviq from your backend. Users see your native UI; Canviq receives the submission server-side.
This approach gives you full control over the UX and is best when you want feedback collection to feel native to your product.
How It Works¶
The API key lives on your backend. It is never shipped in your app binary.
Step 1 — Generate an API Key¶
- Go to Settings → API Keys on your Canviq board
- Click New API Key
- Select the
submissions:writescope - Copy the key — it is shown only once
- Store it in your secrets manager (not in your app or its source code)
Your key will look like:
Step 2 — Add a Feedback Endpoint to Your Backend¶
Your backend receives the form data from your app and forwards it to Canviq. This keeps the API key server-side.
// POST /feedback — your backend endpoint called by your app
app.post('/feedback', async (req, res) => {
const { title, description, type, userEmail, userName } = req.body
const response = await fetch('https://canviq.app/api/submissions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.CANVIQ_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
title,
description,
type, // 'idea' or 'problem'
author_email: userEmail, // for attribution and status notifications
author_name: userName,
source: 'ios-app', // tag for filtering in the Canviq dashboard
}),
})
if (!response.ok) {
return res.status(502).json({ error: 'Failed to submit feedback' })
}
const data = await response.json()
res.json({ id: data.id, url: data.url })
})
import httpx, os
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class FeedbackRequest(BaseModel):
title: str
description: str
type: str # 'idea' or 'problem'
user_email: str | None = None
user_name: str | None = None
@app.post("/feedback")
async def submit_feedback(body: FeedbackRequest):
async with httpx.AsyncClient() as client:
resp = await client.post(
'https://canviq.app/api/submissions',
headers={'Authorization': f'Bearer {os.environ["CANVIQ_API_KEY"]}'},
json={
'title': body.title,
'description': body.description,
'type': body.type,
'author_email': body.user_email,
'author_name': body.user_name,
'source': 'ios-app',
},
)
resp.raise_for_status()
return resp.json()
curl -X POST https://canviq.app/api/submissions \
-H "Authorization: Bearer $CANVIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Offline mode support",
"description": "I want to view my saved items when I lose connectivity and have changes sync when I am back online.",
"type": "idea",
"author_email": "[email protected]",
"author_name": "Jane Smith",
"source": "ios-app"
}'
Step 3 — Call Your Endpoint from the App¶
struct FeedbackPayload: Codable {
let title: String
let description: String
let type: String // "idea" or "problem"
let userEmail: String?
let userName: String?
}
func submitFeedback(
title: String,
description: String,
type: String,
completion: @escaping (Result<Void, Error>) -> Void
) {
guard let url = URL(string: "https://api.yourbackend.com/feedback") else { return }
let payload = FeedbackPayload(
title: title,
description: description,
type: type,
userEmail: currentUser?.email,
userName: currentUser?.displayName
)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONEncoder().encode(payload)
URLSession.shared.dataTask(with: request) { _, response, error in
if let error = error { return completion(.failure(error)) }
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
return completion(.failure(URLError(.badServerResponse)))
}
completion(.success(()))
}.resume()
}
data class FeedbackPayload(
val title: String,
val description: String,
val type: String, // "idea" or "problem"
val userEmail: String?,
val userName: String?
)
suspend fun submitFeedback(
title: String,
description: String,
type: String
) {
val payload = FeedbackPayload(
title = title,
description = description,
type = type,
userEmail = currentUser?.email,
userName = currentUser?.displayName
)
val client = OkHttpClient()
val body = Gson().toJson(payload)
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("https://api.yourbackend.com/feedback")
.post(body)
.build()
client.newCall(request).execute()
}
async function submitFeedback(params: {
title: string
description: string
type: 'idea' | 'problem'
}) {
const response = await fetch('/feedback', { // your backend endpoint
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: params.title,
description: params.description,
type: params.type,
userEmail: currentUser?.email,
userName: currentUser?.displayName,
}),
})
if (!response.ok) throw new Error('Failed to submit feedback')
return response.json() as Promise<{ id: string; url: string }>
}
Submission Fields Reference¶
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | Yes | 5–200 characters |
description | string | Yes | 20–5000 characters |
type | "idea" or "problem" | Yes | Ideas = feature requests, problems = bugs/friction |
author_email | string | No | Used for attribution and to notify the user when status changes |
author_name | string | No | Display name shown on the submission |
source | string | No | Tag for dashboard filtering — e.g. "ios-app", "android", "web" |
category_id | UUID | No | Assign to a board category — fetch IDs from GET /api/organizations/{id}/categories |
Recommended In-App Form Design¶
Keep the form minimal. Two fields is the right amount — more than that and completion rates drop sharply.
What kind of feedback?
○ Idea — "I wish the app could..."
○ Problem — "Something isn't working..."
[Short title] (required, 5–200 chars)
[More detail] (required, 20–5000 chars — hint: "What were you trying to do?")
[Submit]
On success, show a brief confirmation and offer an optional "View it →" link that opens the submission on your board.
Choosing Between the Two Approaches¶
| Embedded form | API submission | |
|---|---|---|
| Backend code required | No | Yes |
| Native UI | No (web) | Yes |
| Users can vote and view roadmap | Yes | No (submission only) |
| Author attribution | Magic link auth | Pass author_email |
| Content moderation | Automatic | Automatic |
| Works offline | No | Yes (queue + sync) |
| Time to ship | ~1 hour | ~2 hours |
Start with the embedded form if you want the fastest path to production. It requires no backend code and gives users the full board experience — they can vote on other submissions, view the roadmap, and follow their own items.
Switch to the API approach if:
- You want feedback collection to feel native to your app's design system
- You need to add custom fields or contextual metadata (e.g., app version, screen name, subscription tier)
- You need offline support with a sync-on-connect queue
Rate Limits¶
| Scenario | Limit |
|---|---|
| API submissions per key | 100 / minute |
| Per-user via embedded board | 5 submissions / hour |
Responses include X-RateLimit-Remaining and X-RateLimit-Reset headers. On 429, back off and retry after the reset timestamp.
Error Handling¶
| Status | Meaning | Action |
|---|---|---|
400 | Validation error — check error field | Fix the request body |
401 | Invalid or missing API key | Verify CANVIQ_API_KEY is set correctly |
403 | Key lacks submissions:write scope | Regenerate key with correct scope |
429 | Rate limited | Back off exponentially and retry |
402 | Plan submission limit reached | Upgrade plan or contact support |
5xx | Canviq server error | Retry with exponential backoff |
Questions¶
Contact [email protected].