Receiptify API Docs
v2.0.0 liveGenerate 1:1 brand receipts for 52 stores. Webhooks, batch sends, receipt history, analytics, and copy-paste bot templates for Discord, Telegram, and web.
Base: https://api.reciptifydev.xyz
Version 2.0.0
Auth: Bearer token
52 brands
49 endpoints
๐ Start Here โ 3-step quickstart
Every integration should follow this flow. validate โ preview โ generate. The first two steps are free and don't consume quota.
STEP 1 ยท FREE
POST /v1/validate
Check brand slug, URL, price, currency. Returns a human-ready
discord_feedback string on any issue โ no parsing needed.STEP 2 ยท FREE
POST /v1/preview
Render the full receipt HTML to base64. Inspect before sending. No email sent.
STEP 3 ยท USES QUOTA
POST /v1/generate
Send the receipt to an email. Set Idempotency-Key to safely retry.
Python โ complete quickstart
import requests
KEY = "rcptfy_live_your_key_here"
BASE = "https://api.reciptifydev.xyz/v1"
AUTH = {"Authorization": f"Bearer {KEY}"}
# โโ Step 1: validate (free) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
vr = requests.post(f"{BASE}/validate", json={
"brand": "nike",
"product_url": "https://www.nike.com/t/air-force-1-07/CW2288-111",
"price": "110.00",
"currency": "ยฃ"
}, headers=AUTH).json()
if not vr["data"]["valid"]:
print(vr["data"]["discord_feedback"]) # ready-to-send error message
exit()
slug = vr["data"]["brand_slug"] # "nike"
# โโ Step 2: preview (free) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
import base64
pr = requests.post(f"{BASE}/preview", json={
"brand": slug, "product_url": "https://www.nike.com/t/air-force-1-07/CW2288-111",
"price": "110.00", "currency": "ยฃ"
}, headers=AUTH).json()
html = base64.b64decode(pr["data"]["html_base64"]).decode()
with open("preview.html", "w") as f: f.write(html)
print(f"Preview saved โ {pr['data']['html_length']} bytes, order #{pr['data']['order_number']}")
# โโ Step 3: generate (uses 1 quota request) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
gr = requests.post(f"{BASE}/generate", json={
"brand": slug,
"product_url": "https://www.nike.com/t/air-force-1-07/CW2288-111",
"price": "110.00",
"currency": "ยฃ",
"email_mode": "normal",
"size": "UK 9",
"recipient_email": "buyer@gmail.com",
"external_id": "order_9281"
}, headers={**AUTH, "Idempotency-Key": "order-nike-110-9281"}).json()
if gr["success"]:
d = gr["data"]
print(f"โ
Sent! Order #{d['order_number']} ยท {d['product_name']} โ {d['sent_to']}")
print(f" Receipt ID: {d['receipt_id']} (use this to resend or look up later)")
else:
e = gr["error"]
print(f"โ [{e['code']}] {e['message']}")
Authentication
Send your API key as a
Key types:
Quota-free endpoints (no request consumed):
Bearer token in every authenticated request:Authorization: Bearer rcptfy_live_abcdefghijklmnop12345678Key types:
rcptfy_live_ โ real key, emails are sentrcptfy_test_ โ test key, receipts are generated but no emails are ever sentQuota-free endpoints (no request consumed):
GET /v1/ping ยท GET /v1/health ยท GET /v1/brands* ยท GET /v1/templates ยท GET /v1/changelog ยท POST /v1/validate ยท POST /v1/preview ยท GET /v1/usage ยท GET /v1/me ยท GET /v1/analytics ยท GET /v1/receipts*
Response Format
Every response follows the same envelope. No exceptions.
Success envelope
{
"success": true,
"request_id": "req_a1b2c3d4e5f6g7h8", // unique per request โ include in bug reports
"data": { ... } // endpoint-specific payload
}Error envelope
{
"success": false,
"request_id": "req_a1b2c3d4e5f6g7h8",
"error": {
"code": "invalid_brand", // stable machine-readable string โ never changes
"message": "Brand slug not recognised.",
"discord_feedback": "โ **Error `invalid_brand`**
Brand slug not recognised.
๐ก Call GET /v1/brands for the full list",
"details": { ... } // optional โ per-field errors on validation failures
}
}Common Error Examples
ERR
โถ
Response
HTTP/1.1 401 Unauthorized
X-Request-Id: req_a1b2c3d4e5f6g7h8
{
"success": false,
"request_id": "req_a1b2c3d4e5f6g7h8",
"error": {
"code": "invalid_api_key",
"message": "Invalid API key. Keys start with rcptfy_live_ or rcptfy_test_.",
"discord_feedback": "โ **Error `invalid_api_key`**
Invalid API key.
๐ก Get a key by joining our Discord and purchasing a plan.",
"details": {"hint": "Get a key by joining our Discord and purchasing a plan."}
}
}๐ก Check the key starts with
rcptfy_live_ or rcptfy_test_ and that you're not including extra whitespace.ERR
โถ
Response
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1748822400
X-Request-Id: req_a1b2c3d4e5f6g7h8
{
"success": false,
"request_id": "req_a1b2c3d4e5f6g7h8",
"error": {
"code": "rate_limit_exceeded",
"message": "Quota exhausted โ 200 of 200 requests used.",
"discord_feedback": "โ **Error `rate_limit_exceeded`**
Quota exhausted.
๐ก Contact support on Discord to upgrade or reset your quota.",
"details": {"requests_used": 200, "requests_limit": 200, "remaining": 0,
"hint": "Contact support on Discord to upgrade or reset your quota."}
}
}๐ก Resets at midnight UTC. Check
X-RateLimit-Reset header for exact timestamp. Use GET /v1/usage to check remaining quota before making requests.ERR
โถ
Response
HTTP/1.1 400 Bad Request
{
"success": false,
"request_id": "req_a1b2c3d4e5f6g7h8",
"error": {
"code": "invalid_brand",
"message": ""nikee" is not a supported brand slug.",
"discord_feedback": "โ **Error `invalid_brand`**
"nikee" is not a supported brand slug.
๐ก See GET /v1/brands for the full list",
"details": {
"brand": "nikee",
"valid_examples": "acnestudios, adidas, amazon, arcteryx, apple...",
"hint": "Call GET /v1/brands for the full list"
}
}
}๐ก Use
GET /v1/brands/search?q=nik to find valid slugs by partial name match.ERR
โถ
Response
HTTP/1.1 422 Unprocessable Entity
{
"success": false,
"request_id": "req_a1b2c3d4e5f6g7h8",
"error": {
"code": "validation_error",
"message": "Request body failed field-level validation.",
"discord_feedback": "โ **Error `validation_error`**
Request body failed field-level validation.
โข **currency**: must be one of: $ ยฃ โฌ ยฅ
โข **price**: Decimal string required e.g. "110.00"",
"details": {
"currency": "must be one of: $ ยฃ โฌ ยฅ",
"price": "Decimal string required e.g. "110.00" โ got "ยฃ110""
}
}
}๐ก Check
error.details for per-field errors. The discord_feedback string is pre-formatted to send directly to a Discord user.ERR
โถ
Response
HTTP/1.1 500 Internal Server Error
{
"success": false,
"request_id": "req_a1b2c3d4e5f6g7h8",
"error": {
"code": "email_send_failed",
"message": "Email delivery failed. Retry once, then contact support.",
"discord_feedback": "โ **Error `email_send_failed`**
Email delivery failed.
๐ก Include request_id when contacting support.",
"details": {"hint": "Include request_id when contacting support."}
}
}๐ก Retry with the same
Idempotency-Key โ if the first attempt actually sent, you'll get the cached success response. If it retries and fails again, contact support with the request_id.Status
GETNo authโถ
Response
{"pong": true, "ts": "2025-06-01T12:00:00Z"}
โน /v1/ping is the only endpoint that returns a bare object (no envelope). All other endpoints use the standard envelope.
GETNo authโถ
Response
{"success":true,"request_id":"req_a1b2c3d4e5f6g7h8","data":{"status":"online","version":"2.0.0","uptime":"4h 22m 11s",
"server_time":"2025-06-01T12:00:00Z","brands":52,"active_keys":41,
"receipts_sent":9823,"receipts_today":142}}GETNo authโถ
cURL
curl https://api.reciptifydev.xyz/v1/changelog
Info
GETNo authโถ
โ Use the slug (e.g.
nike) in POST /v1/generate โ not the display name.cURL
curl https://api.reciptifydev.xyz/v1/brands?region=US
Response shape
{"success":true,"request_id":"req_a1b2c3d4e5f6g7h8","data":{"total":52,"brands":[
{"slug":"nike","name":"Nike","region":"US","active":true}, ...
]}}GETNo authโถ
Examples
curl "https://api.reciptifydev.xyz/v1/brands/search?q=louis" curl "https://api.reciptifydev.xyz/v1/brands/search?q=nik®ion=US"
GETNo authโถ
cURL
curl https://api.reciptifydev.xyz/v1/brands/nike
GETNo authโถ
cURL
curl https://api.reciptifydev.xyz/v1/templates
Account
GETโถ
Response
{"success":true,"request_id":"req_a1b2c3d4e5f6g7h8","data":{"label":"John 1mo","plan":"standard","expires_at":"2025-07-01 00:00:00",
"quota":{"used":47,"limit":500,"remaining":453,"percent_used":9.4,"bar":"[#........] 9.4%"},
"account":{"name":"John Smith","email":"john@gmail.com","country":"UK"}}}PATCHโถ
Body (all optional)
| Field | Example |
|---|---|
| name | John Smith |
| john@gmail.com | |
| street | 123 High Street |
| city | London |
| zipp | SW1A 1AA |
| country | United Kingdom |
cURL
curl -X PATCH https://api.reciptifydev.xyz/v1/me -H "Authorization: Bearer rcptfy_live_..." -H "Content-Type: application/json" -d '{"name":"John Smith","email":"john@gmail.com","city":"London"}'GETโถ
Response
{"success":true,"request_id":"req_a1b2c3d4e5f6g7h8","data":{"used":47,"limit":500,"remaining":453,"percent_used":9.4,
"resets_at":1748822400,"bar":"[#........] 9.4%",
"discord_summary":"**453** requests left out of **500** (91% remaining)"}}GETโถ
Response
{"success":true,"request_id":"req_a1b2c3d4e5f6g7h8","data":{"period_days":30,"total_sent":142,"total_dry_run":5,
"daily":[{"date":"2025-05-01","count":8},{"date":"2025-05-02","count":12}],
"peak_day":"2025-05-14",
"top_brands":[{"slug":"nike","count":41},{"slug":"louisvuitton","count":28}],
"email_modes":{"normal":130,"spoofed":12}}.],
"email_modes":{"normal":130,"spoofed":12}}}Receipt
POSTโถ
Body
| Field | Description | |
|---|---|---|
| brand | REQ | Brand slug e.g. nike |
| product_url | REQ | Full https:// URL |
| price | OPT | Decimal e.g. 110.00 |
| currency | OPT | $ ยฃ โฌ ยฅ |
| email_mode | OPT | normal | spoofed |
Success
{"success":true,"request_id":"req_a1b2c3d4e5f6g7h8","data":{"valid":true,"brand_slug":"nike",
"brand_info":{"slug":"nike","name":"Nike","region":"US","active":true},
"issues":{},"warnings":{},
"discord_feedback":"โ
All fields look good โ safe to call /generate"}}Validation error
{"success":true,"request_id":"req_a1b2c3d4e5f6g7h8","data":{"valid":false,"brand_slug":null,
"issues":{"brand":""nikee" is not a supported brand slug. Valid examples: acnestudios, adidas, amazon..."},
"warnings":{},
"discord_feedback":"โ Fix these before calling /generate:
โข **brand**: "nikee" is not supported."}}POSTโถ
Python โ decode + view in browser
import base64, webbrowser, tempfile
resp = ... # POST /v1/preview response
html = base64.b64decode(resp["data"]["html_base64"]).decode()
with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False) as f:
f.write(html); webbrowser.open(f.name)POSTโถ
Body fields
| Field | Type | Default | Description | |
|---|---|---|---|---|
| brand | REQ | string | โ | Slug from GET /v1/brands, e.g. nike |
| product_url | REQ | string | โ | Full product URL โ name + image auto-scraped |
| price | REQ | string | โ | Decimal, no symbol, e.g. 110.00 |
| currency | OPT | string | $ | $ ยฃ โฌ ยฅ |
| email_mode | OPT | string | normal | normal | spoofed |
| recipient_email | OPT | string | account email | Override send-to address |
| product_name | OPT | string | auto-scraped | Override name |
| product_image | OPT | string | auto-scraped | Override image URL |
| size | OPT | string | โ | e.g. UK 9 |
| color | OPT | string | โ | e.g. White/Black |
| arriving | OPT | string | today | Delivery date DD/MM/YYYY |
| order_date | OPT | string | today | Order date DD/MM/YYYY |
| external_id | OPT | string | โ | Your ref โ echoed in response, filterable in GET /v1/receipts |
| custom_subject | OPT | string | brand default | Override email subject |
| custom_sender | OPT | string | Receiptify | Override sender name (normal mode) |
| dry_run | OPT | bool | false | Render the receipt and validate all fields without sending. Free โ no quota consumed. |
Headers
| Header | Description |
|---|---|
| Authorization | Bearer rcptfy_live_... or rcptfy_test_... |
| Idempotency-Key | Unique string โ only one email ever sent per key, safe to retry |
Full cURL
curl -X POST https://api.reciptifydev.xyz/v1/generate -H "Authorization: Bearer rcptfy_live_..." -H "Idempotency-Key: order-nike-9281" -H "Content-Type: application/json" -d '{
"brand": "nike",
"product_url": "https://www.nike.com/t/air-force-1-07/CW2288-111",
"price": "110.00", "currency": "ยฃ", "email_mode": "normal",
"size": "UK 9", "color": "White",
"recipient_email": "buyer@gmail.com",
"external_id": "order_9281"
}'Success response
{"success":true,"request_id":"req_a1b2c3d4","data":{
"receipt_id":"rcpt_9f2a1b3c","order_number":"847291038",
"brand":"Nike","brand_slug":"nike","product_name":"Air Force 1 '07",
"email_mode":"normal","sent_to":"buyer@gmail.com",
"subject":"Order Received (Nike.com #847291038)",
"created_at":"2025-06-01T12:00:00Z",
"test_mode":false,"dry_run":false,"external_id":"order_9281"
}}Response headers
| Header | Value |
|---|---|
| X-RateLimit-Limit | Total requests on your plan |
| X-RateLimit-Remaining | Remaining requests |
| X-RateLimit-Reset | Unix timestamp โ midnight UTC reset |
| X-Response-Time | Processing time, e.g. 342.1ms |
POSTโถ
cURL
curl -X POST https://api.reciptifydev.xyz/v1/generate/batch -H "Authorization: Bearer rcptfy_live_..." -H "Content-Type: application/json" -d '{"items":[
{"brand":"nike","product_url":"https://...","price":"110.00","currency":"ยฃ"},
{"brand":"stockx","product_url":"https://...","price":"250.00","currency":"$"}
]}'GETโถ
Query params
| Param | Default | Description |
|---|---|---|
| brand | โ | Slug filter |
| status | โ | sent | dry_run |
| external_id | โ | Your reference ID |
| limit | 20 | Max 100 |
| offset | 0 | Pagination |
GETโถ
cURL
curl https://api.reciptifydev.xyz/v1/receipts/rcpt_9f2a1b3c -H "Authorization: Bearer rcptfy_live_..."
GETโถ
cURL
curl https://api.reciptifydev.xyz/v1/receipts/rcpt_9f2a1b3c/html -H "Authorization: Bearer rcptfy_live_..." -o receipt.html
POSTโถ
cURL
curl -X POST "https://api.reciptifydev.xyz/v1/receipts/rcpt_.../resend?recipient_email=other@gmail.com" -H "Authorization: Bearer rcptfy_live_..."
DELETEโถ
cURL
curl -X DELETE https://api.reciptifydev.xyz/v1/receipts/rcpt_... -H "Authorization: Bearer rcptfy_live_..."
Webhooks
Events:
POST to your HTTPS URL. Must respond 2xx within 5 seconds. After 5 consecutive failures, auto-deactivated.
Optionally provide a
receipt.sent ยท receipt.dry_run ยท quota.warningPOST to your HTTPS URL. Must respond 2xx within 5 seconds. After 5 consecutive failures, auto-deactivated.
Optionally provide a
secret โ we'll sign the payload and send X-Receiptify-Signature: sha256=<hmac> so you can verify authenticity.POSTโถ
Body
| Field | Description | |
|---|---|---|
| url | REQ | https:// endpoint to POST events to |
| events | OPT | Comma-separated: receipt.sent,quota.warning |
| secret | OPT | Signing secret for X-Receiptify-Signature |
cURL
curl -X POST https://api.reciptifydev.xyz/v1/webhooks -H "Authorization: Bearer rcptfy_live_..." -H "Content-Type: application/json" -d '{"url":"https://yourbot.com/hooks/rcptfy","events":"receipt.sent,quota.warning","secret":"s3cr3t"}'Payload shapes
// receipt.sent
{"event":"receipt.sent","receipt_id":"rcpt_...","brand":"Nike","brand_slug":"nike",
"order_number":"847291038","sent_to":"buyer@gmail.com","timestamp":"2025-06-01T12:00:00Z"}
// quota.warning (fires at 80%, 90%, 100% of quota)
{"event":"quota.warning","threshold_pct":80,"used":400,"limit":500,"remaining":100}Python Flask โ verify signature
import hmac, hashlib
from flask import request, abort
SECRET = "s3cr3t"
@app.route("/hooks/rcptfy", methods=["POST"])
def webhook():
sig = request.headers.get("X-Receiptify-Signature", "")
expected = "sha256=" + hmac.new(SECRET.encode(), request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(403)
evt = request.json
if evt["event"] == "receipt.sent":
print(f"โ
{evt['brand']} receipt โ order #{evt['order_number']} sent to {evt['sent_to']}")
elif evt["event"] == "quota.warning":
print(f"โ ๏ธ Quota at {evt['threshold_pct']}% โ {evt['remaining']} requests left")
return "OK", 200GETโถ
cURL
curl https://api.reciptifydev.xyz/v1/webhooks -H "Authorization: Bearer rcptfy_live_..."
POSTโถ
cURL
curl -X POST https://api.reciptifydev.xyz/v1/webhooks/wh_abc123/test -H "Authorization: Bearer rcptfy_live_..."
DELETEโถ
cURL
curl -X DELETE https://api.reciptifydev.xyz/v1/webhooks/wh_abc123 -H "Authorization: Bearer rcptfy_live_..."
Admin
All admin endpoints require
X-Admin-Secret: your_secret. Set api_admin_secret in config.json.POSTAdminโถ
Body
| Field | Default | Description | |
|---|---|---|---|
| owner_discord_id | REQ | โ | Discord user ID |
| label | REQ | โ | e.g. John 1 Month Standard |
| plan | OPT | standard | starter | standard | pro |
| test_mode | OPT | false | true = rcptfy_test_, no real emails |
| requests_limit | OPT | 500 | Max requests |
| expires_days | OPT | โ | Days from now. Omit = never. |
cURL
curl -X POST https://api.reciptifydev.xyz/v1/admin/keys/create -H "X-Admin-Secret: your_secret" -H "Content-Type: application/json" -d '{"owner_discord_id":"123456789","label":"John 1 Month","plan":"standard","requests_limit":200,"expires_days":30}'POSTAdminโถ
cURL
curl -X POST https://api.reciptifydev.xyz/v1/admin/keys/bulk-create -H "X-Admin-Secret: your_secret" -H "Content-Type: application/json" -d '{"keys":[
{"owner_discord_id":"111","label":"User A Starter","plan":"starter","requests_limit":50,"expires_days":7},
{"owner_discord_id":"222","label":"User B Standard","plan":"standard","requests_limit":200,"expires_days":30}
]}'PATCHAdminโถ
Body (all optional)
| Field | Description |
|---|---|
| label | New label |
| requests_limit | New quota cap |
| expires_days | New expiry from now. 0 = never. |
| active | false = revoke, true = reactivate |
POSTAdminโถ
cURL
curl -X POST "https://api.reciptifydev.xyz/v1/admin/keys/revoke?key=rcptfy_live_..." -H "X-Admin-Secret: your_secret"
POSTAdminโถ
cURL
curl -X POST "https://api.reciptifydev.xyz/v1/admin/keys/reactivate?key=rcptfy_live_..." -H "X-Admin-Secret: your_secret"
POSTAdminโถ
cURL
curl -X POST "https://api.reciptifydev.xyz/v1/admin/keys/reset-quota?key=rcptfy_live_..." -H "X-Admin-Secret: your_secret"
GETAdminโถ
cURL
curl https://api.reciptifydev.xyz/v1/admin/keys -H "X-Admin-Secret: your_secret"
GETAdminโถ
cURL
curl "https://api.reciptifydev.xyz/v1/admin/keys/rcptfy_live_..." -H "X-Admin-Secret: your_secret"
POSTAdminโถ
cURL
curl -X POST "https://api.reciptifydev.xyz/v1/admin/keys/rcptfy_live_.../note" -H "X-Admin-Secret: your_secret" -H "Content-Type: application/json" -d '{"note":"Paid via PayPal 1 June. 30 day standard plan."}'GETAdminโถ
cURL
curl "https://api.reciptifydev.xyz/v1/admin/keys/rcptfy_live_.../receipts?limit=50" -H "X-Admin-Secret: your_secret"
GETAdminโถ
cURL
curl "https://api.reciptifydev.xyz/v1/admin/search?q=john&active_only=true" -H "X-Admin-Secret: your_secret"
GETAdminโถ
cURL
curl https://api.reciptifydev.xyz/v1/admin/owner/123456789012345678 -H "X-Admin-Secret: your_secret"
GETAdminโถ
cURL
curl "https://api.reciptifydev.xyz/v1/admin/receipts?brand=nike&status=sent&limit=50" -H "X-Admin-Secret: your_secret"
GETAdminโถ
cURL
curl "https://api.reciptifydev.xyz/v1/admin/analytics?days=7" -H "X-Admin-Secret: your_secret"
GETAdminโถ
Response
{"success":true,"request_id":"req_a1b2c3d4e5f6g7h8","data":{
"keys":{"total":41,"active":38,"test":12,"live":26},
"requests":{"total":9823,"today":142},
"receipts":{"total":9801,"today":139,"sent":9650,"dry_run":151},
"top_brands":[{"brand":"nike","count":2401},{"brand":"louisvuitton","count":1893}]
}}POSTAdminโถ
cURL
curl -X POST "https://api.reciptifydev.xyz/v1/admin/db/cleanup?keep_days=7" -H "X-Admin-Secret: your_secret"
Integration Templates
Copy-paste complete working bots for each platform. Replace YOUR_API_KEY with your key from Discord.
๐ discord.py
๐จ discord.js
โ๏ธ Telegram
๐ Website
๐ PHP
bot.py โ discord.py ยท pip install discord.py aiohttp
import os, aiohttp, discord
from discord import app_commands
from discord.ext import commands
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "YOUR_DISCORD_TOKEN")
RECEIPTIFY_KEY = os.getenv("RECEIPTIFY_KEY", "rcptfy_live_YOUR_KEY")
BASE = "https://api.reciptifydev.xyz/v1"
HEADERS = {"Authorization": f"Bearer {RECEIPTIFY_KEY}"}
intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)
async def api_get(path):
async with aiohttp.ClientSession() as s:
async with s.get(f"{BASE}{path}", headers=HEADERS) as r:
return await r.json()
async def api_post(path, body={}, idem=None):
h = {**HEADERS, "Content-Type": "application/json"}
if idem: h["Idempotency-Key"] = idem
async with aiohttp.ClientSession() as s:
async with s.post(f"{BASE}{path}", json=body, headers=h) as r:
return await r.json()
@bot.tree.command(name="receipt", description="Generate a brand receipt")
@app_commands.describe(brand="Brand slug e.g. nike", url="Product URL",
price="Price e.g. 110.00", email="Recipient email",
currency="$ ยฃ โฌ ยฅ (default ยฃ)", size="Size e.g. UK 9")
async def receipt_cmd(interaction: discord.Interaction, brand: str,
url: str, price: str, email: str, currency: str = "ยฃ", size: str = ""):
await interaction.response.defer(ephemeral=True)
# Step 1: validate (free, no quota used)
vr = await api_post("/validate", {"brand": brand, "product_url": url, "price": price, "currency": currency})
if not vr.get("data", {}).get("valid"):
fb = vr.get("data", {}).get("discord_feedback") or vr.get("error", {}).get("discord_feedback", "โ Validation failed")
return await interaction.followup.send(fb, ephemeral=True)
slug = vr["data"]["brand_slug"]
name = vr["data"]["brand_info"]["name"]
body = {"brand": slug, "product_url": url, "price": price, "currency": currency, "recipient_email": email}
if size: body["size"] = size
# Step 2: generate
gr = await api_post("/generate", body, idem=f"{interaction.user.id}-{slug}-{price}")
if gr.get("success"):
d = gr["data"]
e = discord.Embed(title="โ
Receipt Generated", color=0xf97316)
e.add_field(name="Brand", value=name, inline=True)
e.add_field(name="Order #", value=d["order_number"], inline=True)
e.add_field(name="Sent to", value=d["sent_to"], inline=True)
e.add_field(name="Product", value=d["product_name"], inline=False)
e.add_field(name="Price", value=f"{currency}{price}", inline=True)
e.add_field(name="Receipt ID", value=f"`{d['receipt_id']}`", inline=True)
e.set_footer(text=f"Receiptify ยท {'๐งช Test' if d['test_mode'] else 'โ
Live'}")
await interaction.followup.send(embed=e, ephemeral=True)
else:
await interaction.followup.send(gr.get("error", {}).get("discord_feedback", "โ Failed."), ephemeral=True)
@bot.tree.command(name="brands", description="Browse supported brands")
@app_commands.describe(region="US, DE, or AUTH (optional)")
async def brands_cmd(interaction: discord.Interaction, region: str = ""):
await interaction.response.defer(ephemeral=True)
data = await api_get(f"/brands{'?region='+region if region else ''}")
brands = data.get("data", {}).get("brands", [])
desc = "
".join(f"`{b['slug']}` โ {b['name']}" for b in brands[:25])
if len(brands) > 25: desc += f"
...and {len(brands)-25} more"
e = discord.Embed(title=f"๐ฆ Brands ({data['data']['total']})", description=desc, color=0xf97316)
await interaction.followup.send(embed=e, ephemeral=True)
@bot.tree.command(name="usage", description="Check your API quota")
async def usage_cmd(interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
d = (await api_get("/usage")).get("data", {})
e = discord.Embed(title="๐ API Quota", color=0xf97316)
e.add_field(name="Summary", value=d.get("discord_summary","N/A"), inline=False)
e.add_field(name="Bar", value=f"`{d.get('bar','')}`", inline=False)
await interaction.followup.send(embed=e, ephemeral=True)
@bot.tree.command(name="history", description="Your recent receipts")
@app_commands.describe(brand="Filter by brand slug (optional)")
async def history_cmd(interaction: discord.Interaction, brand: str = ""):
await interaction.response.defer(ephemeral=True)
data = await api_get(f"/receipts?limit=5{'&brand='+brand if brand else ''}")
recs = data.get("data", {}).get("receipts", [])
if not recs:
return await interaction.followup.send("No receipts found.", ephemeral=True)
e = discord.Embed(title="๐งพ Recent Receipts", color=0xf97316)
for r in recs:
e.add_field(name=f"{r['brand']} โ #{r['order_number']}",
value=f"`{r['receipt_id']}` ยท {r['price']}{r['currency']} ยท {r['created_at'][:10]}", inline=False)
await interaction.followup.send(embed=e, ephemeral=True)
@bot.tree.command(name="resend", description="Resend a receipt by ID")
@app_commands.describe(receipt_id="Receipt ID starting with rcpt_", email="Override email (optional)")
async def resend_cmd(interaction: discord.Interaction, receipt_id: str, email: str = ""):
await interaction.response.defer(ephemeral=True)
url_path = f"/receipts/{receipt_id}/resend" + (f"?recipient_email={email}" if email else "")
async with aiohttp.ClientSession() as s:
async with s.post(f"{BASE}{url_path}", headers=HEADERS) as r:
data = await r.json()
if data.get("success"):
d = data["data"]
await interaction.followup.send(f"โ
Resent `{receipt_id}` โ Order #{d['order_number']}", ephemeral=True)
else:
await interaction.followup.send(data.get("error",{}).get("discord_feedback","โ Failed."), ephemeral=True)
@bot.event
async def on_ready():
await bot.tree.sync()
print(f"โ
{bot.user} โ slash commands synced")
bot.run(DISCORD_TOKEN)
bot.js โ discord.js v14 ยท npm install discord.js
const { Client, GatewayIntentBits, SlashCommandBuilder, REST,
Routes, EmbedBuilder } = require("discord.js");
const DISCORD_TOKEN = process.env.DISCORD_TOKEN || "YOUR_DISCORD_TOKEN";
const CLIENT_ID = process.env.CLIENT_ID || "YOUR_CLIENT_ID";
const RECEIPTIFY_KEY = process.env.RECEIPTIFY_KEY || "rcptfy_live_YOUR_KEY";
const BASE = "https://api.reciptifydev.xyz/v1";
const HEADERS = { "Authorization": `Bearer ${RECEIPTIFY_KEY}`, "Content-Type": "application/json" };
async function apiGet(path) {
return (await fetch(`${BASE}${path}`, { headers: HEADERS })).json();
}
async function apiPost(path, body = {}, idempotencyKey = null) {
const h = { ...HEADERS };
if (idempotencyKey) h["Idempotency-Key"] = idempotencyKey;
return (await fetch(`${BASE}${path}`, { method:"POST", headers:h, body:JSON.stringify(body) })).json();
}
const commands = [
new SlashCommandBuilder().setName("receipt").setDescription("Generate a brand receipt")
.addStringOption(o => o.setName("brand").setDescription("Brand slug e.g. nike").setRequired(true))
.addStringOption(o => o.setName("url").setDescription("Product URL").setRequired(true))
.addStringOption(o => o.setName("price").setDescription("Price e.g. 110.00").setRequired(true))
.addStringOption(o => o.setName("email").setDescription("Recipient email").setRequired(true))
.addStringOption(o => o.setName("currency").setDescription("$ ยฃ โฌ ยฅ").setRequired(false))
.addStringOption(o => o.setName("size").setDescription("Size e.g. UK 9").setRequired(false)),
new SlashCommandBuilder().setName("brands").setDescription("List brands")
.addStringOption(o => o.setName("region").setDescription("US | DE | AUTH").setRequired(false)),
new SlashCommandBuilder().setName("usage").setDescription("Check API quota"),
new SlashCommandBuilder().setName("history").setDescription("Recent receipts")
.addStringOption(o => o.setName("brand").setDescription("Filter brand").setRequired(false)),
];
new REST().setToken(DISCORD_TOKEN).put(Routes.applicationCommands(CLIENT_ID),
{ body: commands.map(c => c.toJSON()) }).then(() => console.log("โ
Commands registered"));
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.on("interactionCreate", async interaction => {
if (!interaction.isChatInputCommand()) return;
await interaction.deferReply({ ephemeral: true });
const { commandName: cmd } = interaction;
if (cmd === "receipt") {
const brand = interaction.options.getString("brand");
const url = interaction.options.getString("url");
const price = interaction.options.getString("price");
const email = interaction.options.getString("email");
const currency = interaction.options.getString("currency") || "ยฃ";
const size = interaction.options.getString("size") || "";
const vr = await apiPost("/validate", { brand, product_url: url, price, currency });
if (!vr?.data?.valid) {
return interaction.followUp({ content: vr?.data?.discord_feedback || "โ Validation failed", ephemeral: true });
}
const body = { brand: vr.data.brand_slug, product_url: url, price, currency, recipient_email: email };
if (size) body.size = size;
const gr = await apiPost("/generate", body, `${interaction.user.id}-${vr.data.brand_slug}-${price}`);
if (gr?.success) {
const d = gr.data;
const e = new EmbedBuilder().setTitle("โ
Receipt Generated").setColor(0xf97316)
.addFields(
{ name:"Brand", value:vr.data.brand_info.name, inline:true },
{ name:"Order #", value:d.order_number, inline:true },
{ name:"Sent to", value:d.sent_to, inline:true },
{ name:"Product", value:d.product_name, inline:false },
{ name:"Price", value:`${currency}${price}`, inline:true },
{ name:"Receipt ID", value:``${d.receipt_id}``, inline:true },
).setFooter({ text: `Receiptify ยท ${d.test_mode ? "๐งช Test":"โ
Live"}` });
return interaction.followUp({ embeds: [e], ephemeral: true });
}
return interaction.followUp({ content: gr?.error?.discord_feedback || "โ Failed.", ephemeral: true });
}
if (cmd === "brands") {
const region = interaction.options.getString("region") || "";
const data = await apiGet(`/brands${region ? "?region="+region : ""}`);
const lines = data.data.brands.slice(0,25).map(b => ``${b.slug}` โ ${b.name}`).join("
");
return interaction.followUp({
embeds: [new EmbedBuilder().setTitle(`๐ฆ Brands (${data.data.total})`).setDescription(lines).setColor(0xf97316)],
ephemeral: true
});
}
if (cmd === "usage") {
const d = (await apiGet("/usage")).data;
return interaction.followUp({
embeds: [new EmbedBuilder().setTitle("๐ Quota").setColor(0xf97316)
.addFields({ name:"Summary", value:d.discord_summary, inline:false },
{ name:"Bar", value:``${d.bar}``, inline:false })],
ephemeral: true
});
}
if (cmd === "history") {
const brand = interaction.options.getString("brand") || "";
const data = await apiGet(`/receipts?limit=5${brand ? "&brand="+brand : ""}`);
const recs = data.data.receipts;
if (!recs.length) return interaction.followUp({ content:"No receipts.", ephemeral:true });
const e = new EmbedBuilder().setTitle("๐งพ Receipt History").setColor(0xf97316);
recs.forEach(r => e.addFields({ name:`${r.brand} โ #${r.order_number}`,
value:``${r.receipt_id}` ยท ${r.price}${r.currency} ยท ${r.created_at?.slice(0,10)}`, inline:false }));
return interaction.followUp({ embeds: [e], ephemeral: true });
}
});
client.once("ready", () => console.log(`โ
Logged in as ${client.user.tag}`));
client.login(DISCORD_TOKEN);
bot.py โ python-telegram-bot v20+ ยท pip install python-telegram-bot aiohttp
import os, aiohttp
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, ConversationHandler, filters, ContextTypes
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "YOUR_TELEGRAM_TOKEN")
RECEIPTIFY_KEY = os.getenv("RECEIPTIFY_KEY", "rcptfy_live_YOUR_KEY")
BASE = "https://api.reciptifydev.xyz/v1"
HEADERS = {"Authorization": f"Bearer {RECEIPTIFY_KEY}"}
BRAND, URL, PRICE, EMAIL, SIZE = range(5)
async def api_post(path, body={}, idem=None):
h = {**HEADERS, "Content-Type": "application/json"}
if idem: h["Idempotency-Key"] = idem
async with aiohttp.ClientSession() as s:
async with s.post(f"{BASE}{path}", json=body, headers=h) as r:
return await r.json()
async def api_get(path):
async with aiohttp.ClientSession() as s:
async with s.get(f"{BASE}{path}", headers=HEADERS) as r:
return await r.json()
async def start(u: Update, _):
await u.message.reply_text(
"๐ *Receiptify Bot*
"
"/receipt โ Generate a receipt
"
"/brands โ Browse brands
"
"/usage โ Check your quota
"
"/history โ Recent receipts",
parse_mode="Markdown"
)
async def brands(u: Update, ctx):
region = ctx.args[0].upper() if ctx.args else ""
d = await api_get(f"/brands{'?region='+region if region else ''}")
lines = "
".join(f"`{b['slug']}` โ {b['name']}" for b in d["data"]["brands"][:25])
await u.message.reply_text(f"๐ฆ *Brands ({d['data']['total']})*
{lines}", parse_mode="Markdown")
async def usage(u: Update, _):
d = (await api_get("/usage"))["data"]
await u.message.reply_text(
f"๐ *API Quota*
{d['discord_summary']}
`{d['bar']}`",
parse_mode="Markdown"
)
async def history(u: Update, _):
d = await api_get("/receipts?limit=5")
recs = d["data"]["receipts"]
if not recs: return await u.message.reply_text("No receipts found.")
lines = [f"*{r['brand']}* โ #{r['order_number']}
`{r['receipt_id']}` ยท {r['price']}{r['currency']} ยท {r['created_at'][:10]}" for r in recs]
await u.message.reply_text("๐งพ *Recent Receipts*
" + "
".join(lines), parse_mode="Markdown")
async def receipt_start(u: Update, _):
await u.message.reply_text("What *brand*? e.g. `nike`, `louisvuitton`
/cancel to stop.", parse_mode="Markdown")
return BRAND
async def get_brand(u: Update, ctx):
ctx.user_data["brand"] = u.message.text.strip().lower()
await u.message.reply_text("Paste the full *product URL*:", parse_mode="Markdown")
return URL
async def get_url(u: Update, ctx):
ctx.user_data["url"] = u.message.text.strip()
await u.message.reply_text("*Price*? e.g. `110.00` (no symbol):", parse_mode="Markdown")
return PRICE
async def get_price(u: Update, ctx):
ctx.user_data["price"] = u.message.text.strip()
await u.message.reply_text("*Email* to receive receipt:", parse_mode="Markdown")
return EMAIL
async def get_email(u: Update, ctx):
ctx.user_data["email"] = u.message.text.strip()
await u.message.reply_text("*Size*? e.g. `UK 9` โ or `-` to skip:", parse_mode="Markdown")
return SIZE
async def get_size(u: Update, ctx):
size = u.message.text.strip()
brand = ctx.user_data["brand"]
url = ctx.user_data["url"]
price = ctx.user_data["price"]
email = ctx.user_data["email"]
uid = u.effective_user.id
await u.message.reply_text("โณ Generating...")
vr = await api_post("/validate", {"brand": brand, "product_url": url, "price": price})
if not vr["data"]["valid"]:
fb = vr["data"]["discord_feedback"].replace("**", "*")
await u.message.reply_text(fb, parse_mode="Markdown")
return ConversationHandler.END
slug = vr["data"]["brand_slug"]
body = {"brand": slug, "product_url": url, "price": price, "currency": "ยฃ", "recipient_email": email}
if size and size != "-": body["size"] = size
gr = await api_post("/generate", body, idem=f"tg-{uid}-{slug}-{price}")
if gr["success"]:
d = gr["data"]
await u.message.reply_text(
f"โ
*Receipt Sent!*
"
f"๐ท Brand: *{d['brand']}*
"
f"๐ข Order: `{d['order_number']}`
"
f"๐ฆ Product: {d['product_name']}
"
f"๐ง Sent to: `{d['sent_to']}`
"
f"๐ช ID: `{d['receipt_id']}`",
parse_mode="Markdown"
)
else:
await u.message.reply_text(gr["error"]["discord_feedback"].replace("**","*"), parse_mode="Markdown")
return ConversationHandler.END
async def cancel(u: Update, _):
await u.message.reply_text("Cancelled.")
return ConversationHandler.END
def main():
app = Application.builder().token(TELEGRAM_TOKEN).build()
conv = ConversationHandler(
entry_points=[CommandHandler("receipt", receipt_start)],
states={
BRAND: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_brand)],
URL: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_url)],
PRICE: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_price)],
EMAIL: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_email)],
SIZE: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_size)],
},
fallbacks=[CommandHandler("cancel", cancel)],
)
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("brands", brands))
app.add_handler(CommandHandler("usage", usage))
app.add_handler(CommandHandler("history", history))
app.add_handler(conv)
print("โ
Telegram bot running...")
app.run_polling()
if __name__ == "__main__":
main()
widget.html โ drop-in widget, vanilla JS, no dependencies
<!-- Receiptify Widget โ replace rcptfy_live_YOUR_KEY -->
<div id="rcw">
<style>
#rcw *{box-sizing:border-box;font-family:system-ui,sans-serif}
#rcf{background:#0c0c0c;border:1px solid #1e1e1e;border-radius:12px;padding:24px;max-width:420px;margin:0 auto}
#rcf h2{font-size:17px;font-weight:700;color:#fafafa;margin-bottom:3px}
#rcf p{font-size:12px;color:#52525b;margin-bottom:18px}
.rr{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.rf{margin-bottom:12px}
.rf label{display:block;font-size:10px;font-weight:600;color:#a1a1aa;margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px}
.rf input,.rf select{width:100%;background:#080808;border:1px solid #1e1e1e;border-radius:6px;padding:8px 10px;font-size:12px;color:#fafafa;outline:none;transition:border .15s}
.rf input:focus,.rf select:focus{border-color:#f97316}
.rf select option{background:#0c0c0c}
#rcb{width:100%;background:#f97316;color:#fff;font-size:13px;font-weight:700;border:none;border-radius:8px;padding:11px;cursor:pointer;transition:background .15s}
#rcb:hover{background:#ea580c}#rcb:disabled{background:#27272a;cursor:not-allowed}
#rcm{margin-top:12px;padding:10px 13px;border-radius:7px;font-size:12px;display:none;line-height:1.5}
#rcm.ok{background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.2);color:#4ade80}
#rcm.err{background:rgba(239,68,68,.06);border:1px solid rgba(239,68,68,.15);color:#f87171}
</style>
<form id="rcf" onsubmit="rcSubmit(event)">
<h2>๐งพ Generate Receipt</h2>
<p>Enter your details and get a receipt in seconds.</p>
<div class="rf"><label>Brand</label><input id="rcbrand" placeholder="e.g. nike" required/></div>
<div class="rf"><label>Product URL</label><input id="rcurl" type="url" placeholder="https://..." required/></div>
<div class="rr">
<div class="rf"><label>Price</label><input id="rcprice" placeholder="110.00" required/></div>
<div class="rf"><label>Currency</label>
<select id="rccurr"><option value="ยฃ">ยฃ GBP</option><option value="$">$ USD</option>
<option value="โฌ">โฌ EUR</option><option value="ยฅ">ยฅ JPY</option></select></div>
</div>
<div class="rr">
<div class="rf"><label>Email</label><input id="rcemail" type="email" placeholder="you@gmail.com" required/></div>
<div class="rf"><label>Size (optional)</label><input id="rcsize" placeholder="UK 9"/></div>
</div>
<button type="submit" id="rcb">Generate Receipt</button>
<div id="rcm"></div>
</form>
<script>
const KEY = "rcptfy_live_YOUR_KEY";
const BASE = "https://api.reciptifydev.xyz/v1";
const H = { "Authorization": `Bearer ${KEY}`, "Content-Type": "application/json" };
async function post(p, b) {
return (await fetch(`${BASE}${p}`, { method:"POST", headers:H, body:JSON.stringify(b) })).json();
}
function msg(t, type) {
const el = document.getElementById("rcm");
el.innerHTML = t; el.className = type; el.style.display = "block";
}
async function rcSubmit(e) {
e.preventDefault();
const btn = document.getElementById("rcb");
const brand = document.getElementById("rcbrand").value.trim();
const url = document.getElementById("rcurl").value.trim();
const price = document.getElementById("rcprice").value.trim();
const curr = document.getElementById("rccurr").value;
const email = document.getElementById("rcemail").value.trim();
const size = document.getElementById("rcsize").value.trim();
btn.disabled = true; btn.textContent = "Validating...";
const vr = await post("/validate", { brand, product_url:url, price, currency:curr });
if (!vr?.data?.valid) {
msg("โ " + Object.values(vr?.data?.issues||{}).join("<br>"), "err");
btn.disabled = false; btn.textContent = "Generate Receipt"; return;
}
btn.textContent = "Generating...";
const body = { brand:vr.data.brand_slug, product_url:url, price, currency:curr, recipient_email:email };
if (size) body.size = size;
const gr = await post("/generate", body);
if (gr?.success) {
const d = gr.data;
msg(`โ
<b>Receipt sent!</b><br>Order #${d.order_number} ยท ${d.product_name}<br>Sent to ${d.sent_to}`, "ok");
} else {
msg("โ " + (gr?.error?.message||"Generation failed."), "err");
}
btn.disabled = false; btn.textContent = "Generate Receipt";
}
</script>
</div>
receiptify.php โ pure PHP, no dependencies
<?php
/**
* Receiptify PHP Integration
* Requires: PHP 7.4+, curl extension
* Usage: include this file and call Receiptify::generate(...)
*/
class Receiptify {
const API_KEY = "rcptfy_live_YOUR_KEY";
const BASE = "https://api.reciptifydev.xyz/v1";
private static function call(string $method, string $path, array $body = [], string $idem = null): array {
$headers = [
"Authorization: Bearer " . self::API_KEY,
"Content-Type: application/json",
];
if ($idem) $headers[] = "Idempotency-Key: $idem";
$ch = curl_init(self::BASE . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_POSTFIELDS => $body ? json_encode($body) : null,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
return $res ?? ["success" => false, "error" => ["message" => "curl failed"]];
}
public static function validate(string $brand, string $url, string $price): array {
return self::call("POST", "/validate", compact("brand") + ["product_url" => $url, "price" => $price]);
}
public static function generate(string $brand, string $url, string $price,
string $email, string $currency = "ยฃ", string $size = "", string $externalId = ""): array {
$body = ["brand" => $brand, "product_url" => $url, "price" => $price,
"currency" => $currency, "recipient_email" => $email];
if ($size) $body["size"] = $size;
if ($externalId) $body["external_id"] = $externalId;
$idem = "php-" . md5($brand . $price . $email);
return self::call("POST", "/generate", $body, $idem);
}
public static function usage(): array {
return self::call("GET", "/usage");
}
public static function brands(string $region = ""): array {
return self::call("GET", "/brands" . ($region ? "?region=$region" : ""));
}
public static function receipts(int $limit = 10, string $brand = ""): array {
$q = "?limit=$limit" . ($brand ? "&brand=$brand" : "");
return self::call("GET", "/receipts" . $q);
}
}
/* โโโ Usage examples โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Validate first (free)
$vr = Receiptify::validate("nike", "https://www.nike.com/...", "110.00");
if (!$vr["data"]["valid"]) {
die($vr["data"]["discord_feedback"]); // human-readable error
}
// Generate
$gr = Receiptify::generate(
brand: "nike",
url: "https://www.nike.com/t/air-force-1-07/CW2288-111",
price: "110.00",
email: "buyer@gmail.com",
currency: "ยฃ",
size: "UK 9",
externalId: "order_" . uniqid()
);
if ($gr["success"]) {
$d = $gr["data"];
echo "โ
Receipt sent! Order #{$d['order_number']} โ {$d['product_name']}";
} else {
echo "โ " . $gr["error"]["message"];
}
// Check quota
$usage = Receiptify::usage();
echo $usage["data"]["discord_summary"]; // "**453** requests left out of **500**"
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
Error Reference
Every error has a stable error.code and a error.discord_feedback string ready to forward to Discord/Telegram users.
| code | HTTP | Meaning & fix |
|---|---|---|
| invalid_api_key | 401 | Key not found โ check it starts with rcptfy_live_ or rcptfy_test_ |
| key_revoked | 403 | Key revoked โ contact support |
| key_expired | 403 | Key expired โ purchase renewal on Discord |
| rate_limit_exceeded | 429 | Quota exhausted โ upgrade or wait for reset |
| no_account | 400 | No account set up โ run /generate in Discord bot first |
| no_email | 400 | No email on account โ set via /generate โ Settings |
| invalid_brand | 400 | Slug not found โ use GET /v1/brands for valid slugs |
| invalid_region | 400 | Region must be US, DE, or AUTH |
| invalid_webhook_url | 400 | Webhook URL must be https:// |
| validation_error | 422 | Field failed โ check error.details for per-field errors |
| template_missing | 500 | Template not on server โ contact support with request_id |
| email_send_failed | 500 | Transient โ retry once with same Idempotency-Key |
| webhook_delivery_failed | 502 | Your URL didn't respond 2xx within 5s |
| not_found | 404 | Resource not found or not yours |
| batch_too_large | 400 | Max 5 for /batch, 20 for /bulk-create |
| no_changes | 400 | PATCH body had no fields |
| forbidden | 403 | Wrong or missing X-Admin-Secret |
| internal_error | 500 | Unexpected โ include request_id when contacting support |
Rate Limits
Per-key, resets at midnight UTC. Quota-free endpoints (validate, preview, usage, analytics, receipt history, brands) never consume a request. Every response includes:
| Header | Description |
|---|---|
| X-RateLimit-Limit | Total requests on your plan |
| X-RateLimit-Remaining | Remaining requests |
| X-RateLimit-Reset | Unix timestamp โ next reset (midnight UTC) |
| X-Response-Time | Server processing time e.g. 342.1ms |
| X-Idempotent-Replayed | true when response was served from idempotency cache |
Idempotency
Always set
Use any unique string per order โ e.g. user ID + brand + price. If a request times out or fails, retry with the same key โ we replay the cached response without sending a duplicate email.
Keys stored 24 hours. 5xx responses not cached. Same key with different body returns 409.
Idempotency-Key on POST /v1/generate.Use any unique string per order โ e.g. user ID + brand + price. If a request times out or fails, retry with the same key โ we replay the cached response without sending a duplicate email.
Keys stored 24 hours. 5xx responses not cached. Same key with different body returns 409.
Webhook Events
| Event | When fired | Payload includes |
|---|---|---|
| receipt.sent | After every successful email send | receipt_id, brand, order_number, sent_to, timestamp |
| receipt.dry_run | After every dry_run request | receipt_id, brand, order_number, timestamp |
| quota.warning | When usage reaches 80%, 90%, 100% of quota | threshold_pct, used, limit, remaining |