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

  1. Log in to your HPRS dashboard and go to Dashboard > Settings > API.
  2. Click Create New Key, name it, and copy the full key — it is shown only once.
  3. 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

StatusCodeDescription
200OKRequest succeeded.
201CREATEDResource created.
400BAD_REQUESTInvalid parameters.
401UNAUTHORIZEDMissing, invalid, or revoked API key.
403FORBIDDENValid key but insufficient plan or missing scope.
404NOT_FOUNDPromotion or resource not found.
409CONFLICTDuplicate entry (email already entered this promotion) or reused idempotency_key.
413PAYLOAD_TOO_LARGERequest body exceeds the 10KB limit.
422VALIDATION_ERRORRequest body failed validation — see the details array.
429RATE_LIMIT_EXCEEDEDToo many requests — see the Retry-After header.
500INTERNAL_ERRORServer 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

PlanRead / minWrite / minDaily cap
Subscriber603010,000
Lifetime1206050,000

Every response includes rate-limit headers; 429 responses also include Retry-After:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1708200000

The 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.

MethodEndpointDescription
GET/api/v1/contestsList all your promotions
GET/api/v1/contests/:contestIdRetrieve 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)

MethodEndpointDescription
POST/api/v1/promotionsClone 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.

Automation flow: a product-launch trigger generates campaign copy and a hero image, then a single POST to /api/v1/promotions returns a live public page URL.
  1. Trigger fires (e.g. a new product in your store, an RSS item).
  2. AI drafts the title, description and prize.
  3. The image is uploaded to your Supabase bucket (or Cloudinary) → you get its URL.
  4. One POST with status: "active" creates a live promotion.
  5. The 201 response's public_url is 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"
  }
}
Anatomy of a generated promotion: the title becomes the public heading, image_url becomes the hero image, prize sets the prize, and the subheading is inherited from the starter template.

Entries

List & Create Entries

MethodEndpointDescription
GET/api/v1/contests/:contestId/entriesList entries (filter with ?email=)
POST/api/v1/contests/:contestId/entriesCreate 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

MethodEndpointDescription
GET/api/v1/webhooksList webhook subscriptions
POST/api/v1/webhooksCreate a webhook
DELETE/api/v1/webhooks/:webhookIdDelete a webhook

Available events

EventTrigger
entry.createdNew 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 typePlatformDescription
email_signupEmailEnter with email
twitter_followTwitter/XFollow an account
twitter_likeTwitter/XLike a tweet
twitter_retweetTwitter/XRetweet a tweet
twitter_view_postTwitter/XView a post on X
twitter_tweetTwitter/XPost a tweet on X
instagram_followInstagramFollow an account
instagram_view_postInstagramView a post
instagram_share_photoInstagramShare a photo
tiktok_followTikTokFollow an account
tiktok_watchTikTokWatch a video
facebook_visit_pageFacebookVisit a page
facebook_view_postFacebookView a post
facebook_join_groupFacebookJoin a group
discord_joinDiscordJoin a server
twitch_followTwitchFollow a channel
twitch_subscribeTwitchSubscribe to a channel
linkedin_followLinkedInFollow a profile
linkedin_company_followLinkedInFollow a company page
linkedin_shareLinkedInShare campaign content
linkedin_postLinkedInCreate a post
steam_wishlistSteamAdd a game to wishlist
steam_play_gameSteamPlay or own a game
steam_join_groupSteamJoin a group
producthunt_followProduct HuntFollow on Product Hunt
producthunt_voteProduct HuntUpvote on Product Hunt
visit_websiteWebVisit a URL
referral_shareReferralShare referral link
referral_conversion_bonusReferralBonus for referred signups
user_uploadUploadUpload an image
daily_bonusGamificationClaim daily bonus
api_importAPIEntry created via API
oauth_loginAuthRead-only — appears in analytics, not settable via POST
social_shareWebRead-only — generic share tracked automatically
surveyGamificationRead-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")