ampSalerno/amperity-ampy-offline-events
Ampy Offline Events API — mock
A mock realtime event-streaming destination for a fictional retailer, "Ampy." It exists so an Amperity realtime/streaming connector has a faithful target to stream events at — one event at a time — without standing up any real third-party system.
This is not a real vendor. There are no external docs; the spec is this README plus app.py.
What it models
A retailer's conversions/events API that ingests customer behavior as it happens. The connector under test streams events the moment they occur (or in small micro-batches).
Supported event types (exactly these seven)
Display-name forms ("Add to Cart", "Refund Purchase") are accepted and normalized to the token. Any other event_name is rejected with AMPY_UNKNOWN_EVENT_NAME.
Auth — the simplest thing that still validates
A single static API key on the Authorization header. No OAuth, no JWT, no token exchange.
- Get a key (self-service, no signup):
GET /_mock/credentials?label=eric-dev Returns a deterministic api_key bound to tenant eric-dev. Same label → same key, forever (survives Space restarts). Labels use hyphens, not underscores.
- Send events with it:
Authorization: Bearer <api_key>Wrong or missing key → 401. The label is embedded in the key, so the mock validates a presented key on its own — there is no server-side key store.
Public by design: anyone who knows the (default) salt can derive any label's key. Mocks have no real security.
Endpoints
Realtime semantics
- Single or batch. Body may be one event object (the realtime case) or an array. A single event returns a flat
{status, event_id, received_at}; a batch returns{received, accepted, duplicates, rejected, results[]}. - Freshness.
event_time(epoch seconds or ms) must be no more than 7 days old and no more than 5 minutes in the future, elseAMPY_STALE_EVENT_TIME. - Idempotency. Supply
event_idto dedupe. Re-sending a seenevent_idreturnsstatus: duplicateand is not double-counted. (Realtime pipelines retry — dedup matters.) - Identity. Every event needs at least one match key:
- PII, hashed:
email_sha256and/orphone_sha256— SHA-256 hashed for privacy. When present, must be a lowercase 64-char hex digest; a raw email/phone is rejected withAMPY_INVALID_MATCH_KEY_HASH. Normalize before hashing (email: trim + lowercase; phone: E.164). - Non-PII, optional:
customer_id— the retailer's own customer key. Accepted as-is (not hashed, not shape-validated). Can identify on its own. - At least one of the three is required, else
AMPY_MISSING_MATCH_KEY.
Error codes
(For a single-event request, a rejection returns HTTP 422. Inside a batch, each event carries its own status/error_code and the call returns 200.)
Force transport errors for retry testing: X-Mock-Inject-Error: 429|500|503.
Pointing a connector at the mock
Get a key: ./mint-creds.sh <label> [base-url].
Run it
Local:
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
uvicorn app:app --port 7860Validate (Newman):
cd postman && ./run.sh local # or ./run.sh hf after deployDeploy to Hugging Face Spaces (free):
./hf-deploy.sh # see the script header for prerequisitesExample
KEY=$(curl -s "http://127.0.0.1:7860/_mock/credentials?label=eric-dev" | python3 -c "import sys,json;print(json.load(sys.stdin)['api_key'])")
# email hashed for privacy: echo -n "alex@example.com" | shasum -a 256
EMAIL_HASH=$(printf '%s' "alex@example.com" | shasum -a 256 | cut -d' ' -f1)
curl -X POST http://127.0.0.1:7860/v1/events \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"event_name":"purchase","event_time":'"$(date +%s)"',
"event_id":"o-55501",
"match_keys":{"email_sha256":"'"$EMAIL_HASH"'"},
"properties":{"value":129.99,"currency":"USD","order_id":"o-55501"}}'