Store Entries API

Store Entries

Shared Sweeps API  ·  Partner Integration

Stores giveaway entries directly in the database for a customer. This is a headless, write-only endpoint for a trusted server-to-server partner integration — it does not require the Shopify app to be installed and does not read from Shopify.

Base URL https://api.sharedsweepsapp.com
POST /store-entries

Authenticates the caller via an HMAC signature, then records the entry in the customerEntriesTx ledger. Each shop gets a dedicated giveaway (slug api-entries) created on first use and reused permanently, since the entry's giveaway foreign key cannot be null.

Upsert behavior. Entries are upserted by (orderID, customerID, giveawayID) — an existing match updates entriesEarned (200); otherwise a new row is inserted (201). When orderId is omitted, a unique manual-<uuid> is generated, so those calls always insert. The customerEntriesWallet aggregate is not updated by this endpoint.

Authentication

Requests are authenticated with an HMAC-SHA256 signature over the raw request body using a shared secret issued to the partner. No JWT, and the app does not need to be installed.
Required Headers
Header Required Description
X-Timestamp required Unix time in seconds when the request was signed.
X-Signature required Lowercase hex HMAC-SHA256(secret, "<X-Timestamp>.<rawBody>").
Content-Type required application/json

The signed payload is the timestamp and the exact raw body joined by a literal .`${timestamp}.${rawBody}`. Send the same byte string you signed; do not re-serialize the JSON. The timestamp must be within ±5 minutes of server time, and the signature is checked against every active secret (enabling zero-downtime rotation). Any failure returns 401 without revealing which check failed.

Signing Example — Node.js
const crypto = require("crypto"); const body = JSON.stringify({ customerId, entriesEarned, shop, orderId }); const timestamp = Math.floor(Date.now() / 1000).toString(); const signature = crypto .createHmac("sha256", process.env.SHARED_SECRET) .update(`${timestamp}.${body}`) .digest("hex"); await fetch("https://api.sharedsweepsapp.com/store-entries", { method: "POST", headers: { "Content-Type": "application/json", "X-Timestamp": timestamp, "X-Signature": signature, }, body, // send this EXACT string — re-serializing breaks the signature });

Request Body

Field Type Required Description
customerId string | number required Customer identifier. Accepts the numeric ID or gid://shopify/Customer/<id>; the GID prefix is stripped and the numeric form is stored.
entriesEarned number required Number of entries to record. Numeric strings are coerced.
orderId string | number optional Order identifier. Accepts the numeric ID or gid://shopify/Order/<id>; the GID prefix is stripped and the numeric form is stored. When omitted, a synthetic manual-<uuid> is generated.
shop string optional* Shop domain, e.g. store.myshopify.com. *Required unless sent via the X-Shopify-Shop-Domain header.
Example Request
POST /store-entries Content-Type: application/json X-Timestamp: 1749945600 X-Signature: 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b { "customerId": "1234567890", "entriesEarned": 25, "orderId": "gid://shopify/Order/5555555555", "shop": "store.myshopify.com" }

Response

201 Created — new entry  /  200 OK — existing entry updated
{ "success": true, "updated": false, "entry": { "id": 9876, "orderID": "gid://shopify/Order/5555555555", "customerID": "1234567890", "entriesEarned": 25, "giveawayID": 42, "shop": "store.myshopify.com" } }
Field Type Description
success boolean Always true on a 2xx response.
updated boolean true if an existing entry was updated, false if a new one inserted.
entry.id number Primary key of the stored customerEntriesTx row.
entry.orderID string Order ID stored (provided value or synthesized manual-<uuid>).
entry.customerID string The customer identifier.
entry.entriesEarned number The entries recorded.
entry.giveawayID number The dedicated api-entries giveaway ID for the shop.
entry.shop string The shop domain the entry belongs to.
Error Responses
Status Body Cause
400 { "error": "Invalid JSON body" } Request body is not valid JSON.
400 { "error": "Validation failed", "details": {…} } Missing/invalid customerId or entriesEarned.
400 { "error": "Missing shop (…)" } No shop in body or header.
401 { "error": "Unauthorized" } Missing/invalid signature, stale timestamp, or no active secret.
500 { "error": "Internal server error" } Unexpected failure (reported to Sentry).