Quick answer
Get your Public and Private API keys under SETUP > API in the Giveaway Ninja dashboard and add your domains to Allowed Origins. Use the Private key for REST API calls from your server: addUserEntries, addUserEntriesBatch, getUserInfo and listOrders. Use the Public key for Client API calls in the browser, like getUserInfo and calculateEntries. All requests go to https://api.giveaway.ninja.
API Keys are available from the SETUP > API menu in your dashboard.
Public API Key — used in client-side (browser) requests. Safe to expose in JavaScript.
Private API Key — used for server-to-server integrations only. Keep this secret.
Allowed Origins must contain the domains you are using the Public API on (comma-separated). For example: myshop.myshopify.com, mycustomdomain.com.
Allowed IPs for the REST API is optional. Leave it empty and REST calls are accepted from any address. Fill it in and calls with your Private key are accepted only from the listed addresses; every other caller is refused with HTTP 403 Forbidden and the body {"status":"IpNotAllowed","message":"Forbidden"}. Refused calls appear in your API log with the address they came from. Entries are IP addresses, CIDR networks or IPv4 ranges, IPv4 or IPv6, separated by commas or new lines. For example: 203.0.113.10, 198.51.100.0/24, 203.0.113.20-203.0.113.40, 2001:db8::/32. The list does not apply to the Client API.
extensions.shopifycdn.com to Allowed Origins to use the Client API inside Shopify Checkout and Thank-You page extensions.
See how to show earned entries on the checkout and thank-you pages.
All API requests are made to:
https://api.giveaway.ninja
Client API endpoints authenticate via the apiKey field in the JSON request body (Public API Key). The server also verifies the Origin header against your Allowed Origins list.
REST API endpoints authenticate via the apiKey field in the JSON request body (Private API Key). No origin check is performed. If you set Allowed IPs for the REST API in the dashboard, the calling address must also be on that list.
Limits are counted per endpoint and per calling IP address, in a fixed one-minute window that starts with your first call. Every call counts, including calls rejected for another reason, such as an unknown email.
| Endpoint | Limit (per minute, per IP) |
|---|---|
| REST addUserEntries | 120 |
| REST addUserEntriesBatch | 30 calls of up to 50 entrants each (1,500 entrants per minute) |
| REST getUserInfo | 600 |
| REST listOrders | 60 calls of up to 500 orders each |
| Client API (each endpoint) | 240 |
A request over the limit still gets HTTP 200. The body is the TooManyRequests status and, on the REST API, the number of seconds until the window resets:
{ "status": "TooManyRequests", "retryAfterSeconds": 23 }
There is no HTTP 429 and no Retry-After header. Read the status field of every response, and on TooManyRequests wait retryAfterSeconds (or 60 seconds on the Client API) before sending the same request again.
calculateOrderEntries endpoint skips rate limiting when the origin is extensions.shopifycdn.com, since Shopify checkout extensions may fire multiple requests during the checkout flow.
The API refuses requests whose User-Agent header looks like a crawler or a default scripting client. This applies to every endpoint, Client API and REST API alike. Calls made from a browser on your store are never affected, because the browser sends its own User-Agent.
Calls from your server, a terminal or a test tool are refused when they keep the client's default User-Agent. That includes curl, wget, python-requests, python-urllib, aiohttp, axios, node-fetch, okhttp, Java's built-in client, Apache-HttpClient, Go's net/http, Postman, Scrapy and headless browsers (Puppeteer, Playwright, Selenium). A User-Agent containing bot, spider, crawl or scraper is refused too.
A refused request gets HTTP 403 Forbidden with an "Access is denied" page or {"error":"forbidden"} instead of one of the JSON statuses listed on this page. It never reaches your account, so it doesn't appear in LOGS > API.
The fix is to set a User-Agent that names your integration, for example MyStore-Integration/1.0:
# cURL
curl -A "MyStore-Integration/1.0" ...
# Node.js (fetch or axios)
headers: { 'Content-Type': 'application/json', 'User-Agent': 'MyStore-Integration/1.0' }
# Python (requests)
requests.post(url, json=payload, headers={'User-Agent': 'MyStore-Integration/1.0'})
# PHP (cURL)
curl_setopt($ch, CURLOPT_USERAGENT, 'MyStore-Integration/1.0');
curl call returns 403 even with a valid key. Add -A "MyStore-Integration/1.0" (or any User-Agent that names your integration) and you'll get the normal JSON response.
All API requests are logged and can be reviewed from your dashboard under LOGS > API.
The logs show request details including the timestamp, IP address, origin, payload content, and response status. This is useful for debugging integration issues and monitoring API usage.
The Private API Key is used for server-to-server integrations. Keep it secure and never expose it in client-side code. REST endpoints authenticate with the key alone: there is no origin check, and the limits are wider than the Client API's. As an extra safeguard you can restrict the key to your servers' addresses with Allowed IPs; calls from anywhere else then get HTTP 403 Forbidden.
Add entries for a specific user in a giveaway. Use this endpoint to award entries from your own backend logic (e.g. custom purchase flows, CRM events, or third-party integrations). A negative value removes entries.
Prefer no code? Import users and orders from a CSV or send entries to Zapier.
| Parameter | Type | Required | Description |
|---|---|---|---|
| apiKey | string | Required | Your Private API Key |
| giveawayId | string | Required | The giveaway ObjectId |
| string | Required | User's email address | |
| entries | integer | Required | Number of entries to add. Negative to remove. Not zero; at most 1,000,000 either way. |
| description | string | Optional | Why the entries were awarded. Plain text, up to 500 characters, shown next to the entry in the dashboard. Put your own reference here, such as an order number. |
curl -X POST "https://api.giveaway.ninja/api/server/addUserEntries" \
-A "MyStore-Integration/1.0" \
-H "Content-Type: application/json" \
-d '{
"apiKey": "your_private_api_key",
"giveawayId": "your_giveaway_id",
"email": "user@example.com",
"entries": 10,
"description": "Bonus entries for VIP customer"
}'
{
"status": "Success",
"data": {
"totalEntries": 135,
"saleValue": 487.50,
"emailConfirmed": "true",
"referralId": "XYZ1234",
"referralCount": 8
}
}
totalEntries is the user's balance after this call.
apiEntries in the REST getUserInfo response before sending a correction again. A negative value never takes a user below zero entries; the response shows what was actually applied.
| Status | Description |
|---|---|
| PayloadMissing | Request body is empty or malformed |
| TooManyRequests | Rate limit exceeded; see retryAfterSeconds |
| MissingApiKey | The apiKey field is missing |
| InvalidApiKey | The key does not match any account (check you are using the Private key) |
| IpNotAllowed | HTTP 403 Forbidden. Your account has Allowed IPs set and this call did not come from one of them. Your API log shows the address seen |
| EntryValueMissingOrZero | entries is missing, zero or beyond 1,000,000 |
| MissingGivewayId | The giveawayId field is missing |
| EmailMissing | The email field is missing |
| GiveawayOrUserNotFound | The giveaway is not yours, or no user with this email entered it |
Tip: Use https://reqbin.com/curl to run quick tests.
The same as addUserEntries, for up to 50 users of one giveaway in a single call. Use it when a job corrects many users at once. Each user is processed on its own: one unknown email does not stop the others, and the response tells you what happened to each one.
| Parameter | Type | Required | Description |
|---|---|---|---|
| apiKey | string | Required | Your Private API Key |
| giveawayId | string | Required | The giveaway ObjectId |
| users | array | Required | 1 to 50 objects, each with email (required), entries (required, not zero, negative to remove) and description (optional). Applied in list order; a duplicate email is applied twice. |
curl -X POST "https://api.giveaway.ninja/api/server/addUserEntriesBatch" \
-A "MyStore-Integration/1.0" \
-H "Content-Type: application/json" \
-d '{
"apiKey": "your_private_api_key",
"giveawayId": "your_giveaway_id",
"users": [
{ "email": "anna@example.com", "entries": 140, "description": "FX correction order #1001 (CAD)" },
{ "email": "ben@example.com", "entries": -20, "description": "FX correction order #1002 (GBP)" },
{ "email": "nobody@example.com", "entries": 5 }
]
}'
{
"status": "Success",
"data": {
"processed": 3,
"succeeded": 2,
"failed": 1,
"entriesApplied": 120,
"results": [
{ "email": "anna@example.com", "status": "Success", "entries": 140, "totalEntries": 640 },
{ "email": "ben@example.com", "status": "Success", "entries": -20, "totalEntries": 80 },
{ "email": "nobody@example.com", "status": "GiveawayOrUserNotFound" }
]
}
}
| Field | Type | Description |
|---|---|---|
| processed | integer | Number of users in the request |
| succeeded | integer | Users whose entries were applied |
| failed | integer | Users that were skipped (see their status) |
| entriesApplied | integer | Sum of the entries actually applied, after clamping at zero |
| results[].email | string | The email as processed (lower-cased and trimmed) |
| results[].status | string | Success, EmailMissing, EntryValueMissingOrZero or GiveawayOrUserNotFound |
| results[].entries | integer | Entries applied to this user (on Success) |
| results[].totalEntries | integer | The user's balance after this call (on Success) |
The top-level status is Success whenever the request itself was accepted, even if every user failed. Always read results.
| Status | Description |
|---|---|
| PayloadMissing | Request body is empty or malformed |
| TooManyRequests | Rate limit exceeded; see retryAfterSeconds |
| MissingApiKey | The apiKey field is missing |
| InvalidApiKey | The key does not match any account |
| IpNotAllowed | HTTP 403 Forbidden. Your account has Allowed IPs set and this call did not come from one of them. Your API log shows the address seen |
| MissingGivewayId | The giveawayId field is missing |
| UsersMissingOrTooMany | users is missing, empty or has more than 50 items. Nothing was applied. |
| GiveawayOrUserNotFound | The giveaway is not yours. Nothing was applied. |
The server-side version of the Client API getUserInfo: the same user lookup with your Private key, a limit of 600 calls per minute and no origin check. Use it from scripts and backend jobs. On top of the Client API fields it returns apiEntries, the history of entries added or removed through the REST API, so a job can check what it has already sent before sending a correction again.
| Parameter | Type | Required | Description |
|---|---|---|---|
| apiKey | string | Required | Your Private API Key |
| giveawayId | string | Required | The giveaway ObjectId |
| string | Required | User's email address (also accepts phone number) |
curl -X POST "https://api.giveaway.ninja/api/server/getUserInfo" \
-A "MyStore-Integration/1.0" \
-H "Content-Type: application/json" \
-d '{
"apiKey": "your_private_api_key",
"giveawayId": "your_giveaway_id",
"email": "user@example.com"
}'
{
"status": "Success",
"data": {
"email": "user@example.com",
"totalEntries": 640,
"saleValue": 487.50,
"saleCurrency": "USD",
"emailConfirmed": "true",
"referralId": "XYZ1234",
"referralCount": 8,
"orders": [
{
"orderId": "5001",
"orderPoints": "360",
"validation": "{\"OrderId\":\"5001\",\"OrderSource\":\"shopify\",\"OrderPoints\":360,\"TotalPoints\":360,\"AttributionMethod\":\"subtotal\", ...}",
"date": "2026-11-12T14:03:22.1234567Z"
}
],
"apiEntries": [
{ "entries": 140, "description": "FX correction order #5001 (CAD)", "date": "2026-11-12T14:15:02.0000000Z" }
]
}
}
| Field | Type | Description |
|---|---|---|
| string | The user's email as stored | |
| totalEntries | integer | Total entries earned by the user |
| saleValue | decimal | Total sale value attributed to the user |
| saleCurrency | string | Currency of saleValue (your store currency) |
| emailConfirmed | string | "true" if the user clicked the confirmation email |
| referralId | string | Referral code assigned to the user |
| referralCount | integer | Number of users referred |
| orders | array | Purchase orders with entry details, oldest first. Same fields as the Client API, plus date (ISO 8601, UTC) of when the order was processed |
| apiEntries | array | Entries added or removed through addUserEntries and addUserEntriesBatch, oldest first: entries as applied, your description and the date |
| Status | Description |
|---|---|
| PayloadMissing | Request body is empty or malformed |
| TooManyRequests | Rate limit exceeded; see retryAfterSeconds |
| MissingApiKey | The apiKey field is missing |
| InvalidApiKey | The key does not match any account |
| IpNotAllowed | HTTP 403 Forbidden. Your account has Allowed IPs set and this call did not come from one of them. Your API log shows the address seen |
| MissingGivewayId | The giveawayId field is missing |
| EmailMissing | The email field is missing |
| GiveawayOrUserNotFound | The giveaway is not yours, or no user with this email or phone entered it |
The orders Giveaway Ninja has processed for your account since a point in time, oldest first, with the entries each one earned. Use it to reconcile your own records against ours without calling getUserInfo once per customer. Results are paged: follow nextCursor until hasMore is false.
| Parameter | Type | Required | Description |
|---|---|---|---|
| apiKey | string | Required | Your Private API Key |
| since | string | Required | Return orders processed at or after this time. ISO 8601, for example 2026-11-30T00:00:00Z. A value without a time zone is read as UTC. |
| until | string | Optional | Return orders processed at or before this time. Same format. |
| giveawayId | string | Optional | Only orders of this giveaway. Omit for all giveaways of your account, including orders that matched no giveaway. |
| status | string | Optional | Only orders with this status, for example entries. See the status table below. |
| limit | integer | Optional | Orders per page. Default 100, maximum 500. |
| cursor | string | Optional | The nextCursor value from the previous page. Send the other parameters unchanged. |
curl -X POST "https://api.giveaway.ninja/api/server/listOrders" \
-A "MyStore-Integration/1.0" \
-H "Content-Type: application/json" \
-d '{
"apiKey": "your_private_api_key",
"giveawayId": "your_giveaway_id",
"since": "2026-11-30T00:00:00Z",
"limit": 200
}'
{
"status": "Success",
"data": {
"count": 2,
"hasMore": true,
"nextCursor": "639012345678901234-672a1b2c3d4e5f6a7b8c9d0e",
"orders": [
{
"id": "672a1b2c3d4e5f6a7b8c9d0d",
"orderId": "5001",
"date": "2026-11-30T08:12:41.0000000Z",
"status": "entries",
"email": "anna@example.com",
"giveawayId": "your_giveaway_id",
"entries": 360,
"vipSignupBonusEntries": 0,
"orderTotal": 72.00,
"currency": "USD",
"source": "web",
"giftCardAmount": 0,
"validation": "{\"OrderId\":\"5001\",\"OrderPoints\":360,\"TotalPoints\":360,\"AttributionMethod\":\"subtotal\", ...}"
},
{
"id": "672a1b2c3d4e5f6a7b8c9d0e",
"orderId": "5002",
"date": "2026-11-30T08:14:02.0000000Z",
"status": "no-user",
"email": "guest@example.com",
"giveawayId": "",
"entries": 0,
"vipSignupBonusEntries": 0,
"orderTotal": 40.00,
"currency": "USD",
"source": "web",
"giftCardAmount": 0,
"validation": "No matching giveaway user found"
}
]
}
}
| Field | Type | Description |
|---|---|---|
| count | integer | Orders in this page |
| hasMore | boolean | true when another page follows |
| nextCursor | string | Pass as cursor to get the next page. Empty on the last page. |
| orders[].id | string | Our record id. One order can have several records: the original, then a cancellation or rollback. |
| orders[].orderId | string | The order number from your store |
| orders[].date | string | When we processed it (ISO 8601, UTC). This is what since and until filter on, not the time the order was placed. |
| orders[].status | string | See the table below |
| orders[].email | string | Customer email on the order |
| orders[].giveawayId | string | The giveaway credited; empty when no entrant matched |
| orders[].entries | integer | Entries awarded for the order (0 when none) |
| orders[].vipSignupBonusEntries | integer | VIP sign-up bonus granted while processing this order, kept apart from the order's own entries |
| orders[].orderTotal | decimal | Order total as received, in currency (your store currency) |
| orders[].source | string | Where the order was placed, for example web or pos |
| orders[].giftCardAmount | decimal | Gift card amount used on the order |
| orders[].validation | string | The entry calculation details, the same JSON text as orders[].validation in getUserInfo, or a short note for orders that earned nothing |
| Status | Meaning |
|---|---|
| entries | An entrant matched and the order earned entries |
| no-entries | An entrant matched but your rules awarded 0 entries (outside the dates, under the minimum, excluded products) |
| no-user | No entrant with the order's email; nothing awarded |
| processed | Imported or re-processed from the dashboard |
| cancelled | A cancellation record: the order's entries were removed |
| rolled-back-edit, rolled-back-cancelled, rolled-back-reimport | The original record after its entries were taken back, because the order was edited, cancelled or re-imported |
| Status | Description |
|---|---|
| PayloadMissing | Request body is empty or malformed |
| TooManyRequests | Rate limit exceeded; see retryAfterSeconds |
| MissingApiKey | The apiKey field is missing |
| InvalidApiKey | The key does not match any account |
| IpNotAllowed | HTTP 403 Forbidden. Your account has Allowed IPs set and this call did not come from one of them. Your API log shows the address seen |
| InvalidParameter | since is missing or not a date, or until or cursor could not be read |
| GiveawayOrUserNotFound | giveawayId was given and is not one of your giveaways |
async function listOrdersSince(since) {
const all = [];
let cursor = '';
do {
const res = await fetch('https://api.giveaway.ninja/api/server/listOrders', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'User-Agent': 'MyStore-Integration/1.0' },
body: JSON.stringify({ apiKey: process.env.GN_PRIVATE_KEY, giveawayId: 'YOUR_GIVEAWAY_ID', since, limit: 500, cursor })
});
const body = await res.json();
if (body.status === 'TooManyRequests') {
await new Promise(r => setTimeout(r, (body.retryAfterSeconds || 60) * 1000));
continue;
}
if (body.status !== 'Success') throw new Error('listOrders: ' + body.status);
all.push(...body.data.orders);
cursor = body.data.hasMore ? body.data.nextCursor : '';
} while (cursor);
return all;
}
Retrieve a user's giveaway data including total entries, referral information, sale value, and a breakdown of all purchase orders with their entry calculations. Use this when you want to display a user's giveaway status on your storefront.
| Parameter | Type | Required | Description |
|---|---|---|---|
| giveawayId | string | Required | The giveaway ObjectId |
| apiKey | string | Required | Your Public API Key |
| string | Required | User's email address (also accepts phone number) |
{
"status": "Success",
"data": {
"totalEntries": 125,
"saleValue": 487.50,
"emailConfirmed": "true",
"referralId": "XYZ1234",
"referralCount": 8,
"orders": [
{
"orderId": "5001",
"orderPoints": "50",
"validation": "{\"OrderId\":\"5001\",\"OrderSource\":\"shopify\",\"OrderSourceResult\":{\"Description\":\"Valid order\",\"IsValid\":true},\"OrderPoints\":50,\"OrderPointsDescription\":\"Entries per currency spent: 1 points per 3 USD (subtotal)\",\"BundleRulePoints\":0,\"BundleRuleResults\":[],\"PurchaseRulePoints\":10,\"TotalPoints\":60,\"PurchasePeriodResult\":null,\"AovBoosterResult\":null,\"OrderDateRangeResult\":{\"Description\":\"Order within valid date range\",\"IsValid\":true},\"MinimumOrderValueResult\":{\"Description\":\"Minimum order value reached\",\"IsValid\":true},\"AttributionMethod\":\"subtotal\",\"PurchaseRuleResults\":[\"Product X matched: +10 points\"],\"GiftCardAmount\":null,\"GiftCardInfo\":null}"
}
]
}
}
| Field | Type | Description |
|---|---|---|
| totalEntries | integer | Total entries earned by the user |
| saleValue | decimal | Total sale value attributed to the user |
| emailConfirmed | string | "true" if the user clicked the confirmation email |
| referralId | string | Referral code assigned to the user (use with ?njref= querystring) |
| referralCount | integer | Number of users referred |
| orders | array | List of purchase orders with entry details |
| orders[].orderId | string | The Shopify order ID |
| orders[].orderPoints | string | Points earned from this order |
| orders[].validation | string (JSON) | Detailed entry calculation breakdown (TotalPoints, PurchaseRuleResults, BundleRuleResults, etc.) |
| Status | Description |
|---|---|
| PayloadMissing | Request body is empty or malformed |
| TooManyRequests | Rate limit exceeded (240 requests/min per IP). Calling from a server? Use the REST getUserInfo. |
| MissingApiKey | The apiKey field is missing |
| InvalidApiKey | The API key does not match any subscription |
| OriginNotAllowed | The request origin is not in your Allowed Origins list |
| MissingGivewayId | The giveawayId field is missing |
| EmailMissing | The email field is missing |
| GiveawayOrUserNotFound | No user found with this email in the specified giveaway |
fetch('https://api.giveaway.ninja/api/client/getUserInfo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
giveawayId: 'YOUR_GIVEAWAY_ID',
apiKey: 'YOUR_PUBLIC_API_KEY',
email: 'customer@example.com'
})
})
.then(response => response.json())
.then(data => {
if (data.status === 'Success') {
console.log('Total entries:', data.data.totalEntries);
console.log('Referral code:', data.data.referralId);
console.log('Orders:', data.data.orders);
} else {
console.error('API error:', data.status);
}
})
.catch(error => console.error('Request failed:', error));
<div id="giveaway-info">
<p>Loading...</p>
</div>
<script>
(function() {
var container = document.getElementById('giveaway-info');
fetch('https://api.giveaway.ninja/api/client/getUserInfo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
giveawayId: 'YOUR_GIVEAWAY_ID',
apiKey: 'YOUR_PUBLIC_API_KEY',
email: '{{ customer.email }}' // Shopify Liquid variable
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'Success') {
var d = data.data;
container.innerHTML =
'<p><strong>Total Entries:</strong> ' + d.totalEntries + '</p>' +
'<p><strong>Sale Value:</strong> $' + d.saleValue.toFixed(2) + '</p>' +
'<p><strong>Referral Code:</strong> ' + d.referralId + '</p>' +
'<p><strong>Referrals:</strong> ' + d.referralCount + '</p>';
} else if (data.status === 'GiveawayOrUserNotFound') {
container.innerHTML = '<p>No giveaway data found for this user.</p>';
} else {
container.innerHTML = '<p>Error: ' + data.status + '</p>';
}
})
.catch(function(err) {
container.innerHTML = '<p>Could not load giveaway data.</p>';
});
})();
</script>
Calculate the number of entries a single product would earn based on the giveaway's purchase rules, bundle rules, time multipliers, and AOV boosters. Use this on product pages to show customers how many entries they'll earn before purchasing.
| Parameter | Type | Required | Description |
|---|---|---|---|
| giveawayId | string | Required | The giveaway ObjectId |
| apiKey | string | Required | Your Public API Key |
| productId | string | Required | Shopify product ID |
| variantId | string | Required | Shopify variant ID |
| quantity | integer | Required | Quantity (must be > 0) |
| price | integer | Required | Unit price in cents (e.g. $50.00 = 5000) |
{
"status": "Success",
"data": {
"totalEntries": 15,
"ruleType": "quantity",
"ruleEntries": 5,
"ruleQuantity": 1,
"ruleAmount": 0,
"multiplier": 2,
"orderRuleEntries": 10,
"orderRuleAmount": 1,
"hasOrderRule": true,
"rewardAmountSpent": 1,
"isExpired": false,
"bonusOrderEntries": 50,
"bonusOrderEntriesOver": 100.00,
"aovMultiplier": 1,
"isAovBooster": false,
"aovExcludeFromOrderTotal": false
}
}
| Field | Type | Description |
|---|---|---|
| totalEntries | integer | Total product entries calculated |
| ruleType | string|null | Product rule type: "quantity" or "amount", or null if no product rule |
| ruleEntries | integer | Entries awarded per rule threshold |
| ruleQuantity | integer | Quantity threshold (for quantity-based rules) |
| ruleAmount | decimal | Amount threshold (for amount-based rules) |
| multiplier | integer | Active time booster multiplier (1 = no boost) |
| orderRuleEntries | integer | Base entries from order-level rule |
| orderRuleAmount | decimal | Order rule amount threshold (0 = fixed mode) |
| hasOrderRule | boolean | Whether order-level rules apply |
| rewardAmountSpent | integer | Reward mode: 0 = fixed per order, 1 = per amount spent, 2 = product rules only |
| isExpired | boolean | true if the giveaway is not active |
| bonusOrderEntries | integer | Bonus entries for orders over a threshold |
| bonusOrderEntriesOver | decimal | Order value threshold for bonus entries |
| aovMultiplier | integer | AOV booster multiplier (if product is a booster) |
| isAovBooster | boolean | true if this product is an AOV booster |
| aovExcludeFromOrderTotal | boolean | true if this booster's value is excluded from order total calculations |
| Status | Description |
|---|---|
| PayloadMissing | Request body is empty or malformed |
| TooManyRequests | Rate limit exceeded |
| MissingApiKey | The apiKey field is missing |
| InvalidApiKey | The API key does not match any subscription |
| OriginNotAllowed | The request origin is not in your Allowed Origins list |
| MissingGiveawayId | The giveawayId field is missing |
| MissingProductId | The productId field is missing |
| MissingVariantId | The variantId field is missing |
| InvalidQuantity | Quantity must be greater than 0 |
| InvalidPrice | Price cannot be negative |
| GiveawayNotFound | No giveaway found with the given ID |
| NoBoostSalesAction | The giveaway does not have purchase entries enabled |
| InvalidProductId | The product ID is not a valid number |
| InvalidVariantId | The variant ID is not a valid number |
| ValidationFailed | Product did not match any entry rule (response includes data with zero entries) |
// Calculate entries for a single product
// price = unit price in cents (Shopify format: $50.00 = 5000)
fetch('https://api.giveaway.ninja/api/client/calculateEntries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
giveawayId: 'YOUR_GIVEAWAY_ID',
apiKey: 'YOUR_PUBLIC_API_KEY',
productId: '7654321',
variantId: '43210987',
quantity: 1,
price: 5000 // $50.00 in cents
})
})
.then(response => response.json())
.then(data => {
if (data.status === 'Success') {
console.log('Entries for this product:', data.data.totalEntries);
console.log('Rule type:', data.data.ruleType);
console.log('Time multiplier:', data.data.multiplier + 'x');
if (data.data.isExpired) {
console.log('Giveaway has ended');
}
} else {
console.error('API error:', data.status);
}
})
.catch(error => console.error('Request failed:', error));
<div id="entries-badge" style="display:none; padding:10px; background:#f0fff0; border:1px solid #ccc; border-radius:6px;">
<span id="entries-count"></span> entries with this product
</div>
<script>
(function() {
// Replace with your actual values or use Shopify Liquid variables
var productId = '{{ product.id }}';
var variantId = '{{ product.selected_or_first_available_variant.id }}';
var price = {{ product.selected_or_first_available_variant.price }}; // Already in cents in Liquid
fetch('https://api.giveaway.ninja/api/client/calculateEntries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
giveawayId: 'YOUR_GIVEAWAY_ID',
apiKey: 'YOUR_PUBLIC_API_KEY',
productId: productId,
variantId: variantId,
quantity: 1,
price: price
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'Success' && data.data.totalEntries > 0) {
document.getElementById('entries-count').textContent = data.data.totalEntries;
document.getElementById('entries-badge').style.display = 'block';
}
})
.catch(function(err) {
console.error('Failed to load entries:', err);
});
})();
</script>
Calculate entries for multiple products in a single request. Use this on collection pages or grids to efficiently fetch entry data for all visible products at once, instead of making individual API calls per product.
| Parameter | Type | Required | Description |
|---|---|---|---|
| giveawayId | string | Required | The giveaway ObjectId |
| apiKey | string | Required | Your Public API Key |
| products | array | Required | Array of product objects (see below) |
| Parameter | Type | Required | Description |
|---|---|---|---|
| productId | string | Required | Shopify product ID |
| variantId | string | Required | Shopify variant ID |
| quantity | integer | Required | Quantity (must be > 0) |
| price | integer | Required | Unit price in cents (e.g. $50.00 = 5000) |
{
"status": "Success",
"data": [
{
"productId": "7654321",
"variantId": "43210987",
"status": "Success",
"totalEntries": 15,
"ruleType": "quantity",
"ruleEntries": 5,
"ruleQuantity": 1,
"ruleAmount": 0,
"multiplier": 2,
"orderRuleEntries": 10,
"orderRuleAmount": 1,
"hasOrderRule": true,
"rewardAmountSpent": 1,
"isExpired": false,
"bonusOrderEntries": 50,
"bonusOrderEntriesOver": 100.00,
"aovMultiplier": 1,
"isAovBooster": false,
"aovExcludeFromOrderTotal": false
}
]
}
The data array contains one result per product, in the same order as the request. Each result includes all fields from calculateEntries plus productId, variantId, and an individual status per product.
Success, Giveaway-Expired, ValidationFailed, InvalidProductId, or InvalidVariantId.
The top-level status is Success as long as the batch request itself is valid.
| Status | Description |
|---|---|
| PayloadMissing | Request body is empty or malformed |
| TooManyRequests | Rate limit exceeded |
| MissingApiKey | The apiKey field is missing |
| InvalidApiKey | The API key does not match any subscription |
| OriginNotAllowed | The request origin is not in your Allowed Origins list |
| MissingGiveawayId | The giveawayId field is missing |
| MissingProducts | The products array is missing or empty |
| MissingProductId | A product in the array is missing productId |
| MissingVariantId | A product in the array is missing variantId |
| InvalidQuantity | A product has quantity <= 0 |
| InvalidPrice | A product has negative price |
| GiveawayNotFound | No giveaway found with the given ID |
| NoBoostSalesAction | The giveaway does not have purchase entries enabled |
// Calculate entries for multiple products in one request
// Ideal for collection pages to avoid multiple API calls
fetch('https://api.giveaway.ninja/api/client/calculateEntriesBatch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
giveawayId: 'YOUR_GIVEAWAY_ID',
apiKey: 'YOUR_PUBLIC_API_KEY',
products: [
{ productId: '111', variantId: '222', quantity: 1, price: 2500 },
{ productId: '333', variantId: '444', quantity: 1, price: 4900 },
{ productId: '555', variantId: '666', quantity: 1, price: 7500 }
]
})
})
.then(response => response.json())
.then(data => {
if (data.status === 'Success') {
data.data.forEach(function(product) {
console.log('Product ' + product.productId + ': ' +
product.totalEntries + ' entries (' + product.status + ')');
});
}
})
.catch(error => console.error('Request failed:', error));
<!-- Place badges inside your product grid items -->
<div class="product-grid">
<div class="product-card" data-product-id="111" data-variant-id="222" data-price="2500">
<p>Product A - $25.00</p>
<span class="entry-badge" style="display:none;"></span>
</div>
<div class="product-card" data-product-id="333" data-variant-id="444" data-price="4900">
<p>Product B - $49.00</p>
<span class="entry-badge" style="display:none;"></span>
</div>
</div>
<script>
(function() {
var cards = document.querySelectorAll('.product-card');
var products = [];
cards.forEach(function(card) {
products.push({
productId: card.dataset.productId,
variantId: card.dataset.variantId,
quantity: 1,
price: parseInt(card.dataset.price)
});
});
fetch('https://api.giveaway.ninja/api/client/calculateEntriesBatch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
giveawayId: 'YOUR_GIVEAWAY_ID',
apiKey: 'YOUR_PUBLIC_API_KEY',
products: products
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'Success') {
data.data.forEach(function(result, i) {
if (result.status === 'Success' && result.totalEntries > 0) {
var badge = cards[i].querySelector('.entry-badge');
badge.textContent = result.totalEntries + ' entries';
badge.style.display = 'inline-block';
}
});
}
})
.catch(function(err) { console.error('Batch request failed:', err); });
})();
</script>
Calculate the total entries an entire cart/order would earn, including order-level points, product-specific purchase rules, bundle rules, time multipliers, AOV boosters, and minimum order requirements. Designed for checkout and cart pages to show customers the total entries they'll earn for the entire order.
See how purchase-based giveaway entries work for the rules this endpoint applies.
| Parameter | Type | Required | Description |
|---|---|---|---|
| giveawayId | string | Required | The giveaway ObjectId |
| apiKey | string | Required | Your Public API Key |
| cartData | object | Required | Cart data object (see below) |
| Parameter | Type | Required | Description |
|---|---|---|---|
| items | array | Required | Array of cart items |
| subtotal | integer | Required | Cart subtotal in cents (before discounts, shipping, taxes) |
| total | integer | Required | Cart total in cents (after discounts, before shipping/taxes) |
| currency | string | Optional | Currency code (e.g. "USD") |
| Parameter | Type | Required | Description |
|---|---|---|---|
| productId | string | Required | Shopify product ID |
| variantId | string | Required | Shopify variant ID |
| quantity | integer | Required | Quantity of this item in the cart |
| price | integer | Required | Line total in cents (unit price × quantity, e.g. 2 items at $25 = 5000) |
price field in cartData.items is the line total (unit price × quantity) in cents, not the unit price. This differs from calculateEntries where price is the unit price.
{
"status": "Success",
"data": {
"totalEntries": 45,
"orderPoints": 20,
"bundleRulePoints": 10,
"purchaseRulePoints": 15,
"multiplier": 1,
"aovMultiplier": 1,
"calculationBase": 120.00,
"attributionMode": "subtotal",
"isExpired": false,
"hasMinimumOrder": true,
"minimumOrderValue": 50.00,
"minimumOrderReached": true
}
}
| Field | Type | Description |
|---|---|---|
| totalEntries | integer | Total entries for the entire order (0 if minimum order not reached) |
| orderPoints | integer | Points from the order-level rule (based on order value or fixed) |
| bundleRulePoints | integer | Points from matched bundle rules |
| purchaseRulePoints | integer | Points from product-specific purchase rules |
| multiplier | integer | Active time booster multiplier (1 = no boost) |
| aovMultiplier | integer | AOV booster multiplier (stackable across matching products) |
| calculationBase | decimal | Order value used for calculation (after deducting excluded products) |
| attributionMode | string | "subtotal" or "total" — which order value is used |
| isExpired | boolean | true if the giveaway is not active |
| hasMinimumOrder | boolean | true if a minimum order value is configured |
| minimumOrderValue | decimal | Minimum order value required to earn entries |
| minimumOrderReached | boolean | true if the current order meets the minimum |
| Status | Description |
|---|---|
| PayloadMissing | Request body is empty or malformed |
| TooManyRequests | Rate limit exceeded |
| MissingApiKey | The apiKey field is missing |
| InvalidApiKey | The API key does not match any subscription |
| OriginNotAllowed | The request origin is not in your Allowed Origins list |
| MissingGiveawayId | The giveawayId field is missing |
| GiveawayNotFound | No giveaway found with the given ID |
| NoBoostSalesAction | The giveaway does not have purchase entries enabled |
cartData is null or has no items, the API returns a Success response with totalEntries: 0 rather than an error. This lets you safely call the API even before the customer has added items.
// Calculate total order entries from Shopify cart data
// Fetch the current cart, then call the API
fetch('/cart.js')
.then(response => response.json())
.then(cart => {
var cartData = {
items: cart.items.map(item => ({
productId: String(item.product_id),
variantId: String(item.variant_id),
quantity: item.quantity,
price: item.final_line_price // Line total in cents
})),
subtotal: cart.items_subtotal_price,
total: cart.total_price,
currency: cart.currency
};
return fetch('https://api.giveaway.ninja/api/client/calculateOrderEntries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
giveawayId: 'YOUR_GIVEAWAY_ID',
apiKey: 'YOUR_PUBLIC_API_KEY',
cartData: cartData
})
});
})
.then(response => response.json())
.then(data => {
if (data.status === 'Success') {
console.log('Total entries for this order:', data.data.totalEntries);
console.log('Order points:', data.data.orderPoints);
console.log('Bundle points:', data.data.bundleRulePoints);
console.log('Purchase rule points:', data.data.purchaseRulePoints);
if (data.data.hasMinimumOrder && !data.data.minimumOrderReached) {
console.log('Minimum order of $' +
data.data.minimumOrderValue.toFixed(2) + ' not reached');
}
}
})
.catch(error => console.error('Request failed:', error));
<div id="order-entries-banner" style="display:none; padding:12px; background:#e8f5e8; border:1px solid #c3e6c3; border-radius:6px; margin:10px 0;">
<span id="order-entries-text"></span>
</div>
<script>
(function() {
var banner = document.getElementById('order-entries-banner');
var text = document.getElementById('order-entries-text');
// Fetch the Shopify cart
fetch('/cart.js')
.then(function(r) { return r.json(); })
.then(function(cart) {
if (!cart.items || cart.items.length === 0) return;
var cartData = {
items: cart.items.map(function(item) {
return {
productId: String(item.product_id),
variantId: String(item.variant_id),
quantity: item.quantity,
price: item.final_line_price
};
}),
subtotal: cart.items_subtotal_price,
total: cart.total_price,
currency: cart.currency
};
return fetch('https://api.giveaway.ninja/api/client/calculateOrderEntries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
giveawayId: 'YOUR_GIVEAWAY_ID',
apiKey: 'YOUR_PUBLIC_API_KEY',
cartData: cartData
})
});
})
.then(function(r) { return r ? r.json() : null; })
.then(function(data) {
if (!data || data.status !== 'Success') return;
var d = data.data;
if (d.hasMinimumOrder && !d.minimumOrderReached) {
text.textContent = 'Add $' +
(d.minimumOrderValue - d.calculationBase).toFixed(2) +
' more to earn giveaway entries!';
banner.style.background = '#fff8e1';
banner.style.borderColor = '#ffe082';
banner.style.display = 'block';
} else if (d.totalEntries > 0) {
var label = d.totalEntries === 1 ? ' entry' : ' entries';
text.textContent = d.totalEntries + label + ' with this order!';
banner.style.display = 'block';
}
})
.catch(function(err) {
console.error('Order entries error:', err);
});
})();
</script>
Use AI coding assistants like Claude, ChatGPT, or GitHub Copilot to help you build your GiveawayNinja API integration faster.
Download the API reference file below and pass it to your AI assistant so it has full context about all endpoints, request/response formats, and error codes.
Download API Reference.md file aboveThe Public key is for browser calls to the Client API and is checked against your Allowed Origins. The Private key is for server-to-server REST calls and must stay secret. See how to send giveaway entries to Zapier for a no-code option.
Yes. Limits are per endpoint and per IP address, in a one-minute window: 120 calls for addUserEntries, 30 for addUserEntriesBatch, 600 for the REST getUserInfo, 60 for listOrders and 240 for each Client API endpoint. Above the limit you get TooManyRequests with retryAfterSeconds. Server scripts should use the REST API, which has the wider limits. See Rate Limiting.
Open LOGS > API in your dashboard. Each request shows its time, IP, origin, payload and response status. For order-related calls, compare with the order log statuses.
The request was refused by our bot filter because its User-Agent looks like a crawler or a default scripting client, such as curl, python-requests, axios, node-fetch or Postman. Send a User-Agent that names your integration, for example MyStore-Integration/1.0, and avoid the words bot, spider, crawl and scraper. Refused requests never reach your account, so they don't appear in LOGS > API. See User-Agent.
Yes. Add extensions.shopifycdn.com to Allowed Origins. See how to show earned entries on the checkout and thank-you pages.