Welcome to Uplup API
Build powerful applications with our comprehensive REST API. Create interactive wheels, manage entries, track analytics, and more with simple HTTP requests.
Quick Start
Get API Keys
Sign up for any plan (including Free) and generate your API key from the dashboard.
Generate keysMake Requests
Use your API key to authenticate requests to our REST endpoints.
Learn authentication →Build & Ship
Create wheels, manage entries, and integrate our tools into your applications.
Explore endpoints →Try it out
// Bearer Token Authentication
const apiKey = 'uplup_live_your_api_key_here';
const response = await fetch(
'https://api.uplup.com/api/v1/wheels',
{
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
}
);
const data = await response.json();
console.log('Your wheels:', data);What you can build
Interactive Decision Wheels
Create customizable spinning wheels for games, random selection, and decision-making tools.
Entry Management
Dynamically add, remove, and modify wheel entries through API calls or CSV imports.
Real-time Results
Get instant results from wheel spins with detailed analytics and winner tracking.
Webhook Integration
Receive real-time notifications when wheels are spun, winners are selected, or entries change.
Embeddable Widgets
Embed interactive wheels directly into your website or application with our embed API.
Analytics & Insights
Track usage patterns, popular entries, and engagement metrics to optimize your wheels.
API Information
Base URL
https://api.uplup.comAPI Version
Authentication
Authenticate your API requests using Bearer token authentication with your Uplup API key.
API Credentials
Bearer Token Authentication
Every API request requires your API key sent as a Bearer token:
- API Key: Secret token following the format
uplup_[env]_[32_characters] - Send via the
Authorization: Bearer YOUR_API_KEYheader
Your API key is shown only once when created. Store it securely — it cannot be retrieved later.
uplup_live_abc123def456...Production environment - Use for live applications
uplup_test_xyz789abc123...Development environment - Use for testing and development
Authentication Methods
Use Bearer token authentication by including your API key in the Authorization header of every request.
// Bearer Token Authentication (Recommended)
const apiKey = 'uplup_live_abc123def456789...';
// Using fetch with Bearer token
const response = await fetch(
'https://api.uplup.com/api/v1/forms/forms',
{
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
}
);
const data = await response.json();
// Using axios with Bearer token
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.uplup.com',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
// Forms & Quizzes API
const forms = await api.get('/api/v1/forms/forms');
// Wheel Picker API (also accepts Bearer token)
const wheels = await api.get('/api/wheel/wheels');Security Best Practices
Never expose API keys in client-side code
Always make API calls from your backend server. API keys should never be visible in browser code, mobile apps, or public repositories.
Do This
- • Store API keys in environment variables
- • Use HTTPS for all API requests
- • Implement proper error handling
- • Rotate keys regularly
- • Use test keys for development
Avoid This
- • Hardcoding keys in source code
- • Committing keys to version control
- • Sharing keys via email or chat
- • Using live keys in development
- • Making requests from frontend
Error Handling
The Uplup API uses conventional HTTP status codes and returns structured JSON error responses. Every error includes a type, human-readable message, and machine-readable code for programmatic handling.
Error Response Format
All errors follow a consistent structure. The param field is included when the error relates to a specific request parameter.
{
"error": {
"type": "invalid_request_error",
"message": "Parameter 'title' cannot be empty.",
"status": 400,
"code": "validation_error",
"param": "title"
}
}| Field | Type | Description |
|---|---|---|
| type | string | Error category. One of: authentication_error, permission_error, invalid_request_error, rate_limit_error, api_error |
| message | string | Human-readable description of the error |
| status | integer | HTTP status code (matches the response status) |
| code | string | Machine-readable error code for programmatic handling |
| param | string? | The parameter that caused the error (included when applicable) |
Error Types
authentication_error401The API key is missing, malformed, inactive, or expired.
permission_error403The API key is valid but lacks the required scope, or the request is blocked by IP/origin restrictions.
invalid_request_error400, 404, 405, 413, 415The request is invalid — wrong parameters, missing fields, unknown resource, or unsupported method.
rate_limit_error429Too many requests. Check the Retry-After header and back off.
api_error500Something went wrong on our end. These are rare and automatically logged.
HTTP Status Codes
Error Code Reference
Use the code field for programmatic error handling. The message field provides human-readable context but may change — do not match on it.
401Authentication Errors
| Code | Message | Cause |
|---|---|---|
| invalid_api_key | Missing API key. Include your key in the Authorization header: Bearer uplup_live_xxx | No Authorization: Bearer ... header |
| invalid_api_key | Invalid API key format. Expected: uplup_live_[32 hex chars] or uplup_test_[32 hex chars]. | Key does not match required format |
| invalid_api_key | Invalid API key. Check that your key is correct and active. | Key not found, deactivated, or expired |
403Permission Errors
| Code | Message | Cause |
|---|---|---|
| plan_scope_restricted | The '{scope}' scope requires the {Plan} plan or higher. | Your plan does not include this scope. Upgrade to unlock it. |
| missing_scope | This API key does not have the '{scope}' scope. | The key was created with Read Only permissions but a write scope is required. Recreate the key with Read & Write. |
| ip_not_allowed | Request IP address is not in the allowed list for this API key. | The key has an IP allowlist configured and your IP is not on it. |
| origin_not_allowed | Request origin is not in the allowed list for this API key. | The key has an origin allowlist and the request Origin header does not match. |
| webhook_limit_reached | Webhook limit reached. Your plan allows {N} active webhooks. | Maximum active webhooks for your plan. Delete unused webhooks or upgrade. |
400Validation Errors
| Code | Example Message | Cause |
|---|---|---|
| validation_error | Missing required parameter 'title'. | A required field is missing from the request body. |
| validation_error | Parameter 'title' cannot be empty. | A required string field is present but empty. |
| validation_error | Parameter 'title' exceeds maximum length of 500 characters. | A string field exceeds its maximum length. |
| validation_error | Parameter 'presentation_mode' must be one of: traditional, conversational. | An enum field has an invalid value. |
| validation_error | Parameter 'position' must be an integer. | A numeric field received a non-numeric value. |
| validation_error | Parameter 'required' must be a boolean. | A boolean field received a non-boolean value. |
| validation_error | Parameter 'fields' must be an array. | An array field received a non-array value. |
| validation_error | Invalid form_id format. Must be exactly 8 alphanumeric characters. | The form ID in the URL path is malformed. |
| validation_error | Maximum 200 fields allowed per form. | The fields array exceeds the per-form field limit. |
| validation_error | Request body cannot be empty for PATCH. | A PATCH request was sent with no body. |
| validation_error | No valid fields to update. | The request body has no recognized fields for this endpoint. |
| validation_error | Parameter 'url' must use HTTPS for webhook URLs. | Webhook URLs must use HTTPS, not HTTP. |
| validation_error | Parameter 'url' cannot point to localhost or loopback addresses. | Webhook URLs cannot target internal/private addresses (SSRF protection). |
404Not Found Errors
| Code | Example Message | Cause |
|---|---|---|
| resource_not_found | Unknown resource 'xyz'. Available resources: forms, wheels, themes, webhooks, account. | The URL path does not match any API resource. |
| endpoint_not_found | Unknown sub-resource 'xyz'. Available: fields, submissions, analytics, quiz, design, clone, publish. | The sub-path under /forms/{id}/ is not recognized. |
| form_not_found | No form found with ID 'abc12345'. | The form does not exist or belongs to a different brand. |
| field_not_found | No field found with ID 'field-uuid'. | The field ID does not exist in this form. |
| submission_not_found | No submission found with ID 'sub-id'. | The submission does not exist or has been deleted. |
| theme_not_found | No theme found with ID '99'. | The theme does not exist or is not accessible to your brand. |
| webhook_not_found | No webhook found with ID '42'. | The webhook does not exist or belongs to a different API key. |
429Rate Limit Errors
| Code | Message | Retry-After |
|---|---|---|
| rate_limit_exceeded | Per-second rate limit exceeded. Retry after 1 second. | 1 |
| rate_limit_exceeded | Per-minute rate limit exceeded. Retry after 60 seconds. | 60 |
| rate_limit_exceeded | Hourly rate limit exceeded. Retry after N seconds. | Seconds until hour resets |
| monthly_submission_limit_reached | Monthly submission limit reached. Your {Plan} plan allows {N} submissions per month. | — |
OtherAdditional Error Codes
| Status | Code | Cause |
|---|---|---|
| 405 | method_not_allowed | HTTP method not supported. Check the Allow response header for valid methods. |
| 413 | body_too_large | Request body exceeds 1 MB. Reduce the payload size. |
| 415 | unsupported_content_type | Set Content-Type: application/json on POST/PATCH requests. |
| 500 | internal_error | Server error. Safe to retry with exponential backoff. If persistent, contact support. |
Example Error Responses
Missing API key:
HTTP/1.1 401 Unauthorized
{
"error": {
"type": "authentication_error",
"message": "Missing API key. Include your key in the Authorization header: Bearer uplup_live_xxx",
"status": 401,
"code": "invalid_api_key"
}
}Read-only key attempting a write operation:
HTTP/1.1 403 Forbidden
{
"error": {
"type": "permission_error",
"message": "This API key does not have the 'forms:write' scope. Update key permissions in your dashboard.",
"status": 403,
"code": "missing_scope"
}
}Invalid parameter:
HTTP/1.1 400 Bad Request
{
"error": {
"type": "invalid_request_error",
"message": "Parameter 'presentation_mode' must be one of: traditional, conversational.",
"status": 400,
"code": "validation_error",
"param": "presentation_mode"
}
}Rate limit exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
"error": {
"type": "rate_limit_error",
"message": "Per-minute rate limit exceeded. Retry after 60 seconds.",
"status": 429,
"code": "rate_limit_exceeded"
}
}Resource not found:
HTTP/1.1 404 Not Found
{
"error": {
"type": "invalid_request_error",
"message": "No form found with ID 'abc12345'.",
"status": 404,
"code": "form_not_found",
"param": "form_id"
}
}Best Practices
- 1.Switch on
code, notmessage— Error codes are stable. Messages may be refined over time. - 2.Respect
Retry-After— On 429 errors, wait the specified seconds before retrying. Do not retry immediately. - 3.Use exponential backoff for 5xx errors — Start at 1s, double each attempt, cap at 30s. These are transient.
- 4.Check
paramfor validation errors — It tells you exactly which field needs fixing. - 5.Log the
X-Request-Idheader — Include it when contacting support. It uniquely identifies each API request.
const response = await fetch('https://api.uplup.com/api/v1/forms', {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const error = await response.json();
switch (error.error?.code) {
case 'invalid_api_key':
console.error('Check your API key');
break;
case 'missing_scope':
console.error('Key lacks required permissions');
break;
case 'rate_limit_exceeded':
const retryAfter = response.headers.get('Retry-After');
console.error(`Rate limited. Retry in ${retryAfter}s`);
await new Promise(r => setTimeout(r, retryAfter * 1000));
break;
case 'validation_error':
console.error(`Invalid param: ${error.error.param}`);
break;
default:
console.error(error.error?.message);
}
}API access is available on all plans, including Free. Free plan includes read-only access (1,000 req/hr). Write access requires the Starter plan or higher. Webhooks require the Business plan or higher.
Rate Limiting
Limits by Plan
Rate limits are enforced per API key across three windows: per-second (burst), per-minute (short-term), and per-hour (budget). Exceeding any limit returns a 429 error with a Retry-After header indicating which window was exceeded.
| Plan | Access | /Second | /Minute | /Hour | API Keys |
|---|---|---|---|---|---|
| Free | Read Only | 2 | 60 | 1,000 | 1 |
| Starter | Full Access | 5 | 150 | 5,000 | 2 |
| Pro | Full Access | 10 | 300 | 10,000 | 5 |
| Business | Full Access + Webhooks | 10 | 600 | 25,000 | 10 |
| Scale | Full Access + Webhooks | 10 | 1,000 | 50,000 | Unlimited |
| Enterprise | Need higher limits? for custom rate limits. | ||||
Maximum burst rate is 10 requests/second across all plans. Higher plans differ in sustained throughput.
Submission Limits
Submissions count against your monthly limit regardless of source — public form, embed, or API. When your limit is reached, submission endpoints return a 429 error. Read endpoints continue to work normally.
| Plan | Monthly Submissions |
|---|---|
| Free | 100 |
| Starter | 250 |
| Pro | 1,000 |
| Business | 5,000 |
| Scale | 25,000+ |
Submission Limit Error Response
{
"error": {
"type": "rate_limit_error",
"message": "Monthly submission limit reached. Your Free plan allows 100 submissions per month.",
"status": 429,
"code": "monthly_submission_limit_reached",
"details": {
"limit": 100,
"used": 100,
"resets_at": "2026-04-01T00:00:00Z"
}
}
}Scope Access by Plan
Each plan tier determines which API scopes are available. Attempting to use a scope not included in your plan returns a 403 error.
| Scope | Free | Starter | Pro | Business | Scale |
|---|---|---|---|---|---|
| forms:read | ✓ | ✓ | ✓ | ✓ | ✓ |
| forms:write | — | ✓ | ✓ | ✓ | ✓ |
| submissions:read | ✓ | ✓ | ✓ | ✓ | ✓ |
| submissions:write | — | ✓ | ✓ | ✓ | ✓ |
| analytics:read | ✓ | ✓ | ✓ | ✓ | ✓ |
| quiz:read | ✓ | ✓ | ✓ | ✓ | ✓ |
| quiz:write | — | ✓ | ✓ | ✓ | ✓ |
| themes:read | ✓ | ✓ | ✓ | ✓ | ✓ |
| webhooks:read | — | — | — | ✓ | ✓ |
| webhooks:write | — | — | — | ✓ | ✓ |
| account:read | — | — | ✓ | ✓ | ✓ |
| wheels:read | ✓ | ✓ | ✓ | ✓ | ✓ |
| wheels:write | — | ✓ | ✓ | ✓ | ✓ |
Forms & Quizzes API
Full CRUD for forms, quizzes, fields, submissions, analytics, design, webhooks, and account management. All endpoints use Bearer token authentication and return Stripe-style JSON responses.
Base URL
https://api.uplup.com/api/v1/formsAuthentication
All requests require a Bearer token: Authorization: Bearer uplup_live_xxx
API access is available on all plans (Free includes read-only). Manage keys in your dashboard.
Forms
Scope: forms:read / forms:write
List Forms
/formsReturns a paginated list of all forms and quizzes in your account.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms?type=quiz&limit=10', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Create Form
/formsCreate a new form or quiz with optional fields, styling, and quiz settings.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'title': 'Customer Feedback',
'content_type': 'form',
'fields': [
{
'type': 'Short Text',
'title': 'Your name'
},
{
'type': 'Multiple Choice',
'title': 'How did you hear about us?',
'options': [
'Google',
'Social Media',
'Friend'
]
}
]
})
});
const data = await response.json();
console.log(data);Get Form
/forms/{form_id}Retrieve a single form with all its fields, pages, styling, and settings.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Update Form
/forms/{form_id}Update form properties. Only provided fields are modified.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345', {
method: 'PATCH',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'title': 'Updated Title',
'description': 'New description'
})
});
const data = await response.json();
console.log(data);Delete Form
/forms/{form_id}Permanently delete a form and all associated data.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Clone Form
/forms/{form_id}/cloneCreate a duplicate of an existing form with all fields, styling, and settings.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/clone', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Publish / Unpublish
/forms/{form_id}/publishToggle the published status of a form. Published forms are accessible via their public URL.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/publish', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'action': 'publish'
})
});
const data = await response.json();
console.log(data);Fields
Manage individual form fields. Scope: forms:read / forms:write
List Fields
/forms/{form_id}/fieldsGet all fields for a form, ordered by position.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/fields', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Add Field
/forms/{form_id}/fieldsAdd a new field to the form at an optional position.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/fields', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'type': 'Multiple Choice',
'title': 'What is your favorite color?',
'options': [
'Red',
'Blue',
'Green'
],
'required': true,
'position': 2
})
});
const data = await response.json();
console.log(data);Get Field
/forms/{form_id}/fields/{field_id}Retrieve a single field by ID.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/fields/f_a1b2c3', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Update Field
/forms/{form_id}/fields/{field_id}Update field properties. Only provided fields are modified.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/fields/f_a1b2c3', {
method: 'PATCH',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'title': 'Full name',
'required': true
})
});
const data = await response.json();
console.log(data);Delete Field
/forms/{form_id}/fields/{field_id}Remove a field from the form.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/fields/f_a1b2c3', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Reorder Fields
/forms/{form_id}/fields/reorderSet the order of all fields by providing field IDs in the desired sequence.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/fields/reorder', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'field_ids': [
'f_d4e5f6',
'f_a1b2c3',
'f_g7h8i9'
]
})
});
const data = await response.json();
console.log(data);Submissions
Scope: submissions:read / submissions:write
List Submissions
/forms/{form_id}/submissionsGet paginated submissions for a form with optional filtering.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/submissions?limit=10&since=2024-01-01', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Get Submission
/forms/{form_id}/submissions/{submission_id}Retrieve a single submission with all decrypted values.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/submissions/sub_123abc', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Delete Submission
/forms/{form_id}/submissions/{submission_id}Soft-delete a submission (can be recovered).
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/submissions/sub_123abc', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Export Submissions (CSV)
/forms/{form_id}/submissions/exportExport all submissions as a CSV file. Returns raw CSV data.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/submissions/export', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'format': 'csv'
})
});
const data = await response.json();
console.log(data);Analytics
Scope: analytics:read
Analytics Overview
/forms/{form_id}/analytics/overviewGet high-level analytics: total submissions, views, conversion rate, and recent activity.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/analytics/overview', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Submission Trends
/forms/{form_id}/analytics/submissionsTime-series submission data, grouped by day, week, or month.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/analytics/submissions?group_by=week', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Geography Breakdown
/forms/{form_id}/analytics/geographySubmission counts by country/region.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/analytics/geography', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Performance Metrics
/forms/{form_id}/analytics/performanceHourly activity patterns, daily trends, and completion time statistics.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/analytics/performance', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Quiz
Quiz-specific endpoints for scoring, results, and leaderboards. Scope: quiz:read / quiz:write
Get Quiz Settings
/forms/{form_id}/quiz/settingsRetrieve quiz configuration: scoring mode, timing, behavior, results display, retakes, and lead capture.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/quiz/settings', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Update Quiz Settings
/forms/{form_id}/quiz/settingsMerge new settings into existing quiz configuration (deep merge).
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/quiz/settings', {
method: 'PATCH',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'timing': {
'enabled': true,
'time_limit': 600
},
'results': {
'show_correct_answers': true
}
})
});
const data = await response.json();
console.log(data);Quiz Results
/forms/{form_id}/quiz/resultsGet scored quiz submissions with points, percentage, and pass/fail status.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/quiz/results?limit=10', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Leaderboard
/forms/{form_id}/quiz/leaderboardTop scorers ranked by percentage (descending), then by submission time (ascending).
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/quiz/leaderboard?limit=5', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Design & Themes
Scope: forms:read / forms:write and themes:read
Get Design
/forms/{form_id}/designRetrieve the form's styling, background, logo, and presentation settings.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/design', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Update Design
/forms/{form_id}/designUpdate the form's visual styling.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/design', {
method: 'PATCH',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'styling': {
'primaryColor': '#E53E3E',
'fontFamily': 'Poppins'
}
})
});
const data = await response.json();
console.log(data);Apply Theme
/forms/{form_id}/design/themeApply a preset or custom theme to the form.
const response = await fetch('https://api.uplup.com/api/v1/forms/forms/abc12345/design/theme', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'theme_id': 'modern-dark'
})
});
const data = await response.json();
console.log(data);List Themes
/themesGet all available themes (presets and custom).
const response = await fetch('https://api.uplup.com/api/v1/forms/themes', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Get Theme
/themes/{theme_id}Retrieve a single theme's details and styling data.
const response = await fetch('https://api.uplup.com/api/v1/forms/themes/modern-dark', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Webhooks
Subscribe to real-time event notifications via HTTP POST. Requires Business plan or higher. Scope: webhooks:read / webhooks:write
Event Types (19)
Form Lifecycle
form.createdNew form or quiz created
form.updatedForm settings modified
form.deletedForm deleted
form.publishedForm published (live)
form.unpublishedForm taken offline
form.clonedForm duplicated
Submissions
submission.createdNew response received
submission.completedMulti-step form completed
submission.deletedSubmission deleted
Quiz
quiz.completedQuiz scored
quiz.passedPassed with passing score
quiz.failedFailed to reach passing score
quiz.timer_expiredQuiz timer ran out
Fields
field.createdField added to form
field.updatedField modified
field.deletedField removed
Responses
response.startedUser began filling out form
response.page_completedUser completed a page with field data
response.abandonedUser left without completing
Payload Format
All webhook deliveries are sent as POST requests with a JSON body. Submission and quiz events include the full field responses:
{
"event": "submission.created",
"data": {
"form_id": "abc12345",
"submission_id": "sub_xyz789...",
"content_type": "form",
"fields": [
{ "id": "field-uuid-1", "type": "Short Text", "label": "Your Name", "value": "John Doe", "is_lead_capture": false },
{ "id": "field-uuid-2", "type": "Multiple Choice", "label": "Favorite Color", "value": "Blue", "is_lead_capture": false },
{ "id": "field-uuid-3", "type": "Checkboxes", "label": "Interests", "value": ["Sports", "Music"], "is_lead_capture": false },
{ "id": "lead_capture_email", "type": "Lead Capture Email", "label": "Email", "value": "john@example.com", "is_lead_capture": true }
],
"metadata": {
"country": "US",
"region": "California"
}
},
"timestamp": "2026-03-09T15:30:45+00:00"
}Request Headers
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-Uplup-Signature | HMAC-SHA256 hex digest of the raw payload using your webhook secret |
X-Webhook-Event | Event type (e.g. submission.created) |
X-Webhook-ID | Your webhook subscription ID |
User-Agent | Uplup-Webhook/1.0 |
Signature Verification
Verify webhook authenticity by computing the HMAC-SHA256 of the raw request body with your signing secret:
const crypto = require('crypto');
function verifySignature(payload, secret, signatureHeader) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf-8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}Delivery Policy
- Webhook deliveries time out after 5 seconds. Your endpoint should respond quickly.
- Your endpoint must return a 2xx status code to be considered successful.
- Failed deliveries are logged but not retried automatically. Check delivery logs in the dashboard.
- All delivery attempts are recorded with status, response code, and timestamp.
- Webhook URLs must use HTTPS. Private/internal IPs are blocked for security.
List Webhooks
/webhooksGet all webhook subscriptions for the current API key.
const response = await fetch('https://api.uplup.com/api/v1/forms/webhooks', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Create Webhook
/webhooksCreate a webhook endpoint. Subscribes to all 19 events by default, or specify a subset. Returns a signing secret (shown only once).
const response = await fetch('https://api.uplup.com/api/v1/forms/webhooks', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'url': 'https://example.com/webhook',
'events': [
'submission.created',
'submission.completed',
'quiz.completed'
],
'form_id': 'abc12345'
})
});
const data = await response.json();
console.log(data);Get Webhook
/webhooks/{webhook_id}Retrieve webhook details including recent delivery attempts.
const response = await fetch('https://api.uplup.com/api/v1/forms/webhooks/wh_abc123', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Update Webhook
/webhooks/{webhook_id}Modify webhook URL, subscribed events, or active status.
const response = await fetch('https://api.uplup.com/api/v1/forms/webhooks/wh_abc123', {
method: 'PATCH',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'is_active': false
})
});
const data = await response.json();
console.log(data);Delete Webhook
/webhooks/{webhook_id}Deactivate and remove a webhook subscription.
const response = await fetch('https://api.uplup.com/api/v1/forms/webhooks/wh_abc123', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Test Webhook
/webhooks/{webhook_id}/testSend a test event to verify your webhook endpoint is receiving and processing events correctly.
const response = await fetch('https://api.uplup.com/api/v1/forms/webhooks/wh_abc123/test', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Regenerate Secret
/webhooks/{webhook_id}/regenerate-secretGenerate a new signing secret for a webhook. The old secret is immediately invalidated. Store the new secret securely — it is only returned once.
const response = await fetch('https://api.uplup.com/api/v1/forms/webhooks/wh_abc123/regenerate-secret', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Get Deliveries
/webhooks/{webhook_id}/deliveriesRetrieve delivery logs for a webhook. Includes event type, status, response code, and timestamps. Supports pagination.
const response = await fetch('https://api.uplup.com/api/v1/forms/webhooks/wh_abc123/deliveries?limit=10', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Account
Scope: account:read
Get Account
/accountRetrieve account information, plan details, usage metrics, limits, and available features.
const response = await fetch('https://api.uplup.com/api/v1/forms/account', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data);Webhook Signature Verification
Every webhook delivery includes an X-Uplup-Signature header containing an HMAC-SHA256 signature of the request body using your webhook secret. Always verify this signature before processing events.
// Node.js signature verification
const crypto = require('crypto');
function verifyWebhookSignature(body, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your webhook handler:
app.post('/webhook', (req, res) => {
const signature = req.headers['x-uplup-signature'];
const isValid = verifyWebhookSignature(
JSON.stringify(req.body),
signature,
process.env.WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the event
const { event, data } = req.body;
console.log('Received event:', event, data);
res.json({ received: true });
});Rate Limits
Rate limits are enforced per API key across three windows: per-second (burst protection), per-minute (short-term), and per-hour (budget). Exceeding any window returns a 429 error with a Retry-After header indicating which limit was hit.
| Plan | Per Second | Per Minute | Per Hour |
|---|---|---|---|
| Free | 2 | 60 | 1,000 |
| Starter | 5 | 150 | 5,000 |
| Pro | 10 | 300 | 10,000 |
| Business | 10 | 600 | 25,000 |
| Scale | 10 | 1,000 | 50,000 |
| Enterprise | Need higher limits? | ||
The maximum burst rate is 10 requests per second across all plans. Higher plans differ in sustained throughput (per-minute and per-hour budgets).
Response Headers
X-RateLimit-Limit— Maximum requests per hour for your planX-RateLimit-Remaining— Remaining requests in the current hourly windowX-RateLimit-Reset— Unix timestamp when the hourly window resetsRetry-After— Seconds to wait before retrying (only on 429 responses)X-Request-Id— Unique request ID for debugging
Wheel Picker API
Create, manage, and interact with spinning wheels programmatically. Build decision-making tools, games, and interactive experiences.
Base URL: https://api.uplup.com/api/wheel
Ready to get started?
Generate your API keys from the Uplup dashboard and start building amazing applications.
List Wheels
/api/wheel/wheelsRetrieve a paginated list of all wheels belonging to your account.
// Get all wheels with pagination
const apiKey = 'YOUR_API_KEY';
const response =
await fetch('https://api.uplup.com/api/wheel/wheels', {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
// Success - API returns JSON response
// Format: {"success": true, "data": {"wheels": [...]}}
console.log('Response:', JSON.stringify(data, null, 2));
} else {
// Handle error
console.error('Error:', response.status);
if (data.error) {
console.error('Message:', data.error.message);
}
}Create Wheel
/api/wheel/wheelsCreate a new spinning wheel with customizable entries, settings, and appearance.
// Create a new wheel
const wheelData = {
wheel_name: "Team Lunch Picker",
entries: ["Pizza Palace", "Burger Barn", "Taco Town", "Sushi Spot"],
settings: {
spinnerDuration: "normal",
selectedColorSet: "Vibrant",
colorSequence: ["#e6194b", "#3cb44b", "#ffe119", "#4363d8"],
selectedAudio: "https://uplup-media.ams3.digitaloceanspaces.com/root/drum-roll-long.mp3",
volume: 100,
showTitle: true,
removeAfterWin: false,
winnersToSelect: 1
}
};
const apiKey = 'YOUR_API_KEY';
const response = await fetch(
'https://api.uplup.com/api/wheel/wheels', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(wheelData) // Follows redirects automatically
});
const result = await response.json();
if (response.ok) {
// Success - API returns JSON response
// Format: {"success": true, "data": {"wheel_id": "wheel_123", "message": "Wheel created successfully"}}
console.log('Response:', JSON.stringify(result, null, 2));
} else {
// Handle error
console.error('Error:', response.status);
if (result.error) {
console.error('Message:', result.error.message);
}
}Get Wheel
/api/wheel/wheels/{wheel_id}Retrieve detailed information about a specific wheel including all entries and settings.
// Get detailed wheel information
const apiKey = 'YOUR_API_KEY';
const wheelId = 'wheel_123';
const response = await fetch(
`https://api.uplup.com/api/wheel/wheels/${wheelId}`, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
}) // Follows redirects automatically;
const wheel = await response.json();
if (response.ok) {
// Success - API returns JSON response
// Format: {"success": true, "data": {wheel details}}
console.log('Response:', JSON.stringify(wheel, null, 2));
} else {
// Handle error
console.error('Error:', response.status);
if (wheel.error) {
console.error('Message:', wheel.error.message);
}
}Spin Wheel (Coming Soon)
/api/wheel/wheels/{wheel_id}/spinProgrammatically spin a wheel and get the random result. Optionally save the result and trigger webhooks. Note: This endpoint is not yet implemented and will return a 501 error.
// Spin a wheel and get the result
const apiKey = 'YOUR_API_KEY';
const wheelId = 'wheel_123';
const response = await fetch(
`https://api.uplup.com/api/wheel/wheels/${wheelId}/spin`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
save_result: true,
notify_webhooks: true
}) // Follows redirects automatically
});
const result = await response.json();
if (response.ok) {
// Success - API returns JSON response
// Format: {"success": true, "data": {"winner": {...}, "results": [...]}}
console.log('Response:', JSON.stringify(result, null, 2));
} else {
// Handle error
console.error('Error:', response.status);
if (result.error) {
console.error('Message:', result.error.message);
}
}Update Wheel
/api/wheel/wheels/{wheel_id}Update an existing wheel's properties including title, entries, settings, and appearance.
// Update wheel name and add new entries
const updates = {
wheel_name: "Updated Team Lunch Picker",
entries: ["Pizza Palace", "Burger Barn", "Sushi Spot", "Deli Downtown"],
settings: {
spinnerDuration: "short",
enableConfetti: false
}
};
const apiKey = 'YOUR_API_KEY';
const wheelId = 'wheel_123';
const response = await fetch(
`https://api.uplup.com/api/wheel/wheels/${wheelId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updates) // Follows redirects automatically
});
const result = await response.json();
if (response.ok) {
// Success - API returns JSON response
// Format: {"success": true, "data": {"wheel_id": "wheel_123", "message": "Wheel created successfully"}}
console.log('Response:', JSON.stringify(result, null, 2));
} else {
// Handle error
console.error('Error:', response.status);
if (result.error) {
console.error('Message:', result.error.message);
}
}Delete Wheel
/api/wheel/wheels/{wheel_id}Permanently delete a wheel and all its associated data including spin history.
// Delete a wheel permanently
const apiKey = 'YOUR_API_KEY';
const wheelId = 'wheel_123';
const response = await fetch(
`https://api.uplup.com/api/wheel/wheels/${wheelId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
}) // Follows redirects automatically;
const result = await response.json();
if (response.ok) {
// Success - API returns JSON response
// Format: {"success": true, "message": "Wheel deleted successfully"}
console.log('Response:', JSON.stringify(result, null, 2));
} else {
// Handle error
console.error('Error:', response.status);
if (result.error) {
console.error('Message:', result.error.message);
}
}Manage Entries (Coming Soon)
/api/wheel/wheels/{wheel_id}/entriesAdd, update, or remove entries from a wheel without replacing the entire entries array. Note: This endpoint is not yet implemented and will return a 501 error.
// Add new entries to a wheel
const entryUpdate = {
action: "add",
entries: ["Mediterranean Cafe", "BBQ Joint", "Vegan Kitchen"]
};
const apiKey = 'YOUR_API_KEY';
const wheelId = 'wheel_123';
const response = await fetch(
`https://api.uplup.com/api/wheel/wheels/${wheelId}/entries`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(entryUpdate) // Follows redirects automatically
});
const result = await response.json();
if (response.ok) {
// Success - API returns JSON response
// Format: {"success": true, "data": {"entries_added": 3, ...}}
console.log('Response:', JSON.stringify(result, null, 2));
} else {
// Handle error
console.error('Error:', response.status);
if (result.error) {
console.error('Message:', result.error.message);
}
}
// Remove specific entries
const removeUpdate = {
action: "remove",
entries: ["entry_id_1", "entry_id_2"]
};