Public API Reference
The HPRS Public API lets you manage promotions, create entries programmatically, pull analytics and leaderboards, and receive webhooks when participants enter. All endpoints are versioned under /api/v1 and return JSON. A machine-readable OpenAPI 3.0 spec is also available.
Quick Start
Getting Started
- Log in to your HPRS dashboard and go to Dashboard > Settings > API.
- Click Create New Key, name it, and copy the full key — it is shown only once.
- Verify the key works:
curl -X GET "https://hprs.co/api/v1/me" \
-H "Authorization: Bearer YOUR_API_KEY"Then list your promotions and create your first entry:
curl -X GET "https://hprs.co/api/v1/contests" \
-H "Authorization: Bearer YOUR_API_KEY"
curl -X POST "https://hprs.co/api/v1/contests/YOUR_CONTEST_ID/entries" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "name": "Test User", "points": 5}'Auth
Authentication
The base URL is https://hprs.co/api/v1. Every request must include your API key in the Authorization header:
Authorization: Bearer hprs_k_x8Kp2mNq4rT6vW9yB1dF3gH5jL7nP0sU- Keys start with
hprs_k_followed by 32 random characters. - Up to 5 active keys per account; keys access all promotions you own.
- The full key is shown only once at creation — store it securely.
- Revoke keys anytime from the dashboard; revocation is immediate.
- API access requires an active subscription or lifetime plan (trial accounts receive 403).
Errors
Error Handling
| Status | Code | Description |
|---|---|---|
| 200 | OK | Request succeeded. |
| 201 | CREATED | Resource created. |
| 400 | BAD_REQUEST | Invalid parameters. |
| 401 | UNAUTHORIZED | Missing, invalid, or revoked API key. |
| 403 | FORBIDDEN | Valid key but insufficient plan or missing scope. |
| 404 | NOT_FOUND | Promotion or resource not found. |
| 409 | CONFLICT | Duplicate entry (email already entered this promotion) or reused idempotency_key. |
| 413 | PAYLOAD_TOO_LARGE | Request body exceeds the 10KB limit. |
| 422 | VALIDATION_ERROR | Request body failed validation — see the details array. |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests — see the Retry-After header. |
| 500 | INTERNAL_ERROR | Server error. |
Errors always use a structured body:
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "You have exceeded the rate limit of 60 requests per minute.",
"status": 429
}
}Validation failures (422) include a details array:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request body failed validation.",
"status": 422,
"details": [
{ "field": "email", "message": "Email is required." },
{ "field": "points", "message": "Points must be a positive integer." }
]
}
}Limits
Rate Limiting
| Plan | Read / min | Write / min | Daily cap |
|---|---|---|---|
| Subscriber | 60 | 30 | 10,000 |
| Lifetime | 120 | 60 | 50,000 |
Every response includes rate-limit headers; 429 responses also include Retry-After:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1708200000The daily cap counts all requests (reads and writes) per account within a UTC calendar day. Exceeding it returns 429 RATE_LIMIT_EXCEEDED with Retry-After set to the seconds until UTC midnight.
- Cache responses when possible and fetch in pages.
- Implement exponential backoff on 429.
- Use webhooks instead of polling for real-time updates.
Lists
Pagination
All list endpoints accept page (default 1) and per_page (default 25, max 100) and return a pagination object:
{
"success": true,
"data": [ ... ],
"pagination": { "page": 1, "per_page": 25, "total_pages": 4, "total_count": 87 }
}Account
GET /api/v1/me
Verifies your API key and returns account information including plan, today's API usage, and the number of active promotions (the active_contests field name is retained for backwards compatibility).
{
"success": true,
"data": {
"user_id": "usr_a1b2c3d4",
"email": "you@example.com",
"plan": "lifetime",
"api_usage": {
"requests_today": 142,
"daily_limit": 50000,
"rate_limit_per_minute": 120
},
"active_contests": 12
}
}Promotions
List & Retrieve Promotions
These read endpoints live under /api/v1/contests/… for backwards compatibility — they return your promotions, and the id returned by POST /api/v1/promotions works with all of them.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/contests | List all your promotions |
| GET | /api/v1/contests/:contestId | Retrieve a specific promotion |
The list endpoint supports status (draft, active, ended, paused) and mode (competition, gamification, sharing_only) filters plus pagination. A promotion object looks like:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Win a Nintendo Switch",
"status": "active",
"mode": "competition",
"type": "promotion",
"contest_url": "win-nintendo-switch",
"start_date": "2026-02-01T00:00:00Z",
"end_date": "2026-03-01T23:59:59Z",
"days_left": 12,
"total_entries": 847,
"entry_methods": [
{ "label": "Enter with Email", "points": 5, "actionType": "email_signup" },
{ "label": "Follow us on Twitter", "points": 3, "actionType": "twitter_follow" }
],
"settings": { "template": "basic-new", "color": "#6366F1" },
"daily_bonus_enabled": true,
"referral_enabled": true,
"dark_mode_enabled": false,
"created_at": "2026-01-25T10:30:00Z",
"updated_at": "2026-02-15T14:22:00Z"
}Promotions
Create a Promotion (Clone)
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/promotions | Clone a promotion you own, with copy overrides (write scope) |
Creation is clone-based: keep one polished master promotion per page shape, then let the API copy its template, theme, entry methods and settings verbatim while you override the marketing copy. Body fields: source_promotion_id (optional — omit to clone the platform starter template), title (required — sets the dashboard name AND the public headline), description, prize (sets the prize — renames the first or creates one), end_date(defaults to the source's original duration), campaign_url, image_url (hero image — a Supabase storage or Cloudinary URL), status (draft default, active goes live immediately), and idempotency_key (reuse returns 409 CONFLICTwith the existing promotion's id). Limited to 20 API-created promotions per account per UTC day.
fetch("https://hprs.co/api/v1/promotions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
source_promotion_id: "MASTER_PROMOTION_ID",
title: "Win a Nintendo Switch 2 — launch week special",
prize: "Nintendo Switch 2 + 3 games",
end_date: "2026-07-31T23:59:59Z",
status: "active",
idempotency_key: "n8n-run-8823",
}),
});201 returns the live page URL directly — no follow-up lookup needed:
{
"success": true,
"data": {
"id": "9a1b2c3d-e5f6-7890-abcd-ef1234567890",
"title": "Win a Nintendo Switch 2 — launch week special",
"status": "active",
"mode": "competition",
"type": "promotion",
"contest_url": "x8Kp2m",
"public_url": "https://hprs.co/contest/x8Kp2m",
"edit_url": "https://hprs.co/dashboard/promotion/edit?id=9a1b2c3d-…",
"source_promotion_id": "MASTER_PROMOTION_ID",
"end_date": "2026-07-31T23:59:59Z",
"created_at": "2026-07-02T14:00:00Z"
}
}How automation uses this
The common flow is “a new product drops → launch a campaign”. Your automation (n8n, Zapier, Make) fires on a trigger, generates the copy and a hero image, then makes a single call. Omit source_promotion_id and the platform starter template is cloned for you — so a brand-new account with nothing to clone still works.

- Trigger fires (e.g. a new product in your store, an RSS item).
- AI drafts the
title,descriptionandprize. - The image is uploaded to your Supabase bucket (or Cloudinary) → you get its URL.
- One POST with
status: "active"creates a live promotion. - The 201 response's
public_urlis posted to your channels — no follow-up lookup.
// No source_promotion_id → clones the platform starter template
fetch("https://hprs.co/api/v1/promotions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
title: "Win a Nintendo Switch 2",
image_url:
"https://YOUR_PROJECT.supabase.co/storage/v1/object/public/hprs-public/media/switch.jpg",
prize: "Nintendo Switch 2",
description: "<p><b>Launch-week giveaway — enter free.</b></p>",
status: "active",
}),
});201 — the clone inherits the template's design (template, theme, entry methods, subheading) and applies your copy and image:
{
"success": true,
"data": {
"id": "0d1e2f3a-4b5c-6d7e-8f90-1a2b3c4d5e6f",
"title": "Win a Nintendo Switch 2",
"status": "active",
"type": "promotion",
"contest_url": "x8Kp2m",
"public_url": "https://hprs.co/contest/x8Kp2m",
"edit_url": "https://hprs.co/dashboard/promotion/edit?id=0d1e2f3a-…",
"source_promotion_id": "STARTER_TEMPLATE_ID",
"created_at": "2026-07-03T14:00:00Z"
}
}
Entries
List & Create Entries
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/contests/:contestId/entries | List entries (filter with ?email=) |
| POST | /api/v1/contests/:contestId/entries | Create an entry (write scope) |
POST creates a participant programmatically. Body fields: email (required), name, action_type (default api_import), points, value, and metadata (custom key-value pairs). Creating a duplicate (same email and promotion) returns 409 CONFLICT.
fetch("https://hprs.co/api/v1/contests/CONTEST_ID/entries", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
email: "newuser@example.com",
name: "Alice Johnson",
action_type: "email_signup",
points: 5,
value: "Shopify Order #98765",
metadata: { order_total: "49.99", source: "shopify" },
}),
});Successful creation returns 201 with the new entry:
{
"success": true,
"data": {
"id": "e5f6a7b8-c9d0-1234-ef01-345678901234",
"email": "newuser@example.com",
"full_name": "Alice Johnson",
"entry_points": 5,
"action_type": "email_signup",
"referral_code": "qR5tUv",
"created_at": "2026-02-17T12:00:00Z"
}
}Analytics
GET /api/v1/contests/:contestId/analytics
Aggregated promotion analytics: totals, action breakdown, daily entry counts, top countries, and referral stats.
{
"success": true,
"data": {
"contest_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"total_entries": 847,
"unique_participants": 312,
"total_points_awarded": 4235,
"actions_breakdown": { "email_signup": 312, "twitter_follow": 198 },
"daily_entries": [ { "date": "2026-02-10", "count": 45 } ],
"top_countries": [ { "country_code": "US", "country_name": "United States", "count": 156 } ],
"referral_stats": {
"total_referral_clicks": 234,
"total_referral_conversions": 45,
"conversion_rate": 0.192
}
}
}Leaderboard
GET /api/v1/contests/:contestId/leaderboard
Participants ranked by points (descending), paginated.
{
"success": true,
"data": [
{
"rank": 1,
"full_name": "John Doe",
"email": "john@example.com",
"entry_points": 85,
"referral_code": "mN3pQr",
"country_code": "GB"
}
],
"pagination": { "page": 1, "per_page": 10, "total_pages": 32, "total_count": 312 }
}Surveys
GET /api/v1/contests/:contestId/survey-responses
Survey responses collected from gamified promotions, newest first, paginated.
{
"success": true,
"data": [
{
"id": "f6a7b8c9-d0e1-2345-f012-456789012345",
"contest_user_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"response": "I love gaming and would play every day!",
"question_index": 0,
"points_awarded": 3,
"is_completed": true,
"created_at": "2026-02-12T16:45:00Z"
}
],
"pagination": { "page": 1, "per_page": 25, "total_pages": 5, "total_count": 109 }
}Webhooks
Webhooks
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/webhooks | List webhook subscriptions |
| POST | /api/v1/webhooks | Create a webhook |
| DELETE | /api/v1/webhooks/:webhookId | Delete a webhook |
Available events
| Event | Trigger |
|---|---|
| entry.created | New participant enters (via widget or API). |
contest.ended, winner.selected and daily_bonus.claimed are reserved names that are not yet emitted. Subscribing to them returns 422 VALIDATION_ERROR rather than accepting a subscription that would never deliver.
Create a webhook
Provide an HTTPS url and at least one event. The response includes the signing secret — shown only once:
curl -X POST "https://hprs.co/api/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://yourserver.com/webhooks/hprs", "events": ["entry.created"]}'
{
"success": true,
"data": {
"id": "w1h2k3i4-d5e6-7890-abcd-ef1234567890",
"url": "https://yourserver.com/webhooks/hprs",
"events": ["entry.created"],
"secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"is_active": true,
"created_at": "2026-02-17T12:00:00Z"
}
}Payload format
{
"event": "entry.created",
"timestamp": "2026-02-17T12:00:00Z",
"data": {
"contest_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"entry_id": "e5f6a7b8-c9d0-1234-ef01-345678901234",
"email": "newuser@example.com",
"full_name": "Alice Johnson",
"points": 5,
"action_type": "email_signup"
}
}Signature verification
Every delivery is signed with HMAC-SHA256 in the X-HPRS-Signature header. Verify it against the raw request body using your webhook secret:
const crypto = require("crypto");
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(payload, "utf8")
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}Delivery behavior
- Your endpoint must respond with a 2xx status within 10 seconds.
- Failed deliveries are retried with backoff: 5s, 30s, then 5 minutes.
- Subscriptions are auto-disabled after 10 consecutive failures.
- Webhook URLs must use HTTPS; redirects are treated as failures.
Reference
Action Types
| Action type | Platform | Description |
|---|---|---|
| email_signup | Enter with email | |
| twitter_follow | Twitter/X | Follow an account |
| twitter_like | Twitter/X | Like a tweet |
| twitter_retweet | Twitter/X | Retweet a tweet |
| twitter_view_post | Twitter/X | View a post on X |
| twitter_tweet | Twitter/X | Post a tweet on X |
| instagram_follow | Follow an account | |
| instagram_view_post | View a post | |
| instagram_share_photo | Share a photo | |
| tiktok_follow | TikTok | Follow an account |
| tiktok_watch | TikTok | Watch a video |
| facebook_visit_page | Visit a page | |
| facebook_view_post | View a post | |
| facebook_join_group | Join a group | |
| discord_join | Discord | Join a server |
| twitch_follow | Twitch | Follow a channel |
| twitch_subscribe | Twitch | Subscribe to a channel |
| linkedin_follow | Follow a profile | |
| linkedin_company_follow | Follow a company page | |
| linkedin_share | Share campaign content | |
| linkedin_post | Create a post | |
| steam_wishlist | Steam | Add a game to wishlist |
| steam_play_game | Steam | Play or own a game |
| steam_join_group | Steam | Join a group |
| producthunt_follow | Product Hunt | Follow on Product Hunt |
| producthunt_vote | Product Hunt | Upvote on Product Hunt |
| visit_website | Web | Visit a URL |
| referral_share | Referral | Share referral link |
| referral_conversion_bonus | Referral | Bonus for referred signups |
| user_upload | Upload | Upload an image |
| daily_bonus | Gamification | Claim daily bonus |
| api_import | API | Entry created via API |
| oauth_login | Auth | Read-only — appears in analytics, not settable via POST |
| social_share | Web | Read-only — generic share tracked automatically |
| survey | Gamification | Read-only — survey response, not settable via POST |
Examples
Integration Examples
Shopify: award an entry on purchase
async function handleShopifyOrder(order) {
const CONTEST_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
const response = await fetch(
`https://hprs.co/api/v1/contests/${CONTEST_ID}/entries`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
email: order.customer.email,
name: `${order.customer.first_name} ${order.customer.last_name}`,
points: 10,
value: `Shopify Order #${order.order_number}`,
metadata: { order_total: order.total_price, source: "shopify" },
}),
}
);
const result = await response.json();
console.log("Entry created:", result.data.id);
}Python: list active promotions
import requests
API_KEY = "hprs_k_..."
response = requests.get(
"https://hprs.co/api/v1/contests",
params={"status": "active"},
headers={"Authorization": f"Bearer {API_KEY}"}
)
for promotion in response.json()["data"]:
print(f"{promotion['title']} - {promotion['total_entries']} entries")