Authentication
All API requests require a Bearer token in the Authorization header:
curl -X POST https://tryperkspring.com/api/verify \ -H "Authorization: Bearer pk_your_api_key" \ -H "Content-Type: application/json"
API keys are scoped to your organization and environment (sandbox or live). Create and manage keys from the dashboard or via the API.
API Conventions
Versioning
Send a PerkSpring-Version header with a date-based version (e.g. 2026-06-18). Your org is pinned to a version on first request. All responses include PerkSpring-Version and X-API-Version-Current headers.
Idempotency
For POST requests, send an Idempotency-Key header (max 256 chars). If you retry with the same key within 24 hours, PerkSpring returns the cached response with an Idempotent-Replayed: true header.
Rate Limiting
The verification endpoint is rate-limited to 100 requests per minute per organization. If you exceed the limit, you'll receive a 429 Too Many Requests response.
Sandbox Testing
Sandbox API keys return canned responses for these test emails:
| Result | |
|---|---|
| [email protected] | verified: true |
| [email protected] | verified: true |
| [email protected] | verified: false |
| [email protected] | verified: false |
| [email protected] | 500 error |
| [email protected] | delayed response |
/api/verify
RetailerVerify if an email belongs to a membership organization's roster.
Request
{
"email": "[email protected]",
"membershipOrgId": "5739777b-8a89-417d-..."
}Response
{
"verified": true,
"membershipOrgId": "5739777b-8a89-417d-..."
}The email is hashed server-side with the membership org's salt and compared against stored hashes. Plain-text emails are never stored or logged. Requires an active partnership between your organization and the target membership org.
Connecting over MCP
AI shopping agents do not use the REST endpoints above. They connect to the PerkSpring MCP server over the Model Context Protocol using Streamable HTTP, and call tools rather than endpoints.
Authentication is the same bearer scheme, but the key must belong to an agent account — retailer and membership keys are rejected on this surface, because they are scoped to their own organization's data.
Authorization: Bearer pk_your_agent_api_key
Your MCP endpoint URL is issued with your agent account. Two tools are open to any authenticated agent; check_perks additionally requires that the retailer has approved your agent from their dashboard.
discover_offers
AgentBrowse live offers by category. Takes no personal data, so an agent can explore before it knows who it is shopping for.
Arguments
{
"category": "flowers",
"membership_org_id": "optional-uuid",
"limit": 50,
"offset": 0
}Returns
{
"offers": [
{
"perk_id": "...",
"brand": { "id": "...", "name": "Petal & Stem", "category": "flowers" },
"summary": "20% off",
"discount": { "type": "percent_off", "percent_off": 20, "currency": "USD" },
"available_to": [{ "id": "...", "name": "Veterans Association" }],
"redemption_url": "https://..."
}
],
"returned": 1
}Only offers that are live, inside their validity window, and granted to an active partnership are returned. category matches on prefix, case-insensitively; omit it to see everything, which is also how you learn the available vocabulary. No verification token is issued and no code is minted — eligibility is not asserted by this call.
check_perks
AgentConfirm what one shopper qualifies for at one retailer.
Arguments
{
"email": "[email protected]",
"retailer_id": "retailer-uuid"
}Returns
{
"verified": true,
"perks": [ { "perk_id": "...", "summary": "20% off", "...": "..." } ],
"verification_token": "eyJhbGciOi..."
}The email is hashed per organization and discarded. Requires the retailer to have authorized your agent — an unauthorized call is refused rather than answered, since answering "not a member" would itself disclose something about the shopper. Probe rates are limited per day and per email address.
redeem_perk
AgentIssue a one-time redemption code for one of the perks returned by check_perks.
Arguments
{
"verification_token": "eyJhbGciOi...",
"perk_id": "one of the perk_ids from check_perks",
"transaction_ref": "your-order-reference"
}Returns
{
"code": "PSP-XXXX-XXXX",
"perk_id": "...",
"expires_at": "2026-01-01T00:00:00.000Z"
}Perks do not stack: one verification token yields exactly one code, enforced by a unique database constraint rather than by application logic. Redeeming a different perk_id against an already-used token is rejected, not treated as a second grant. Do not put personal data in transaction_ref — it is stored as given.
/api/upload
MembershipUpload a CSV roster of member emails. Each email is salted, hashed (SHA-256), and stored. Processing runs in the background.
Request
curl -X POST https://tryperkspring.com/api/upload \ -H "Authorization: Bearer pk_your_api_key" \ -F "[email protected]"
CSV format: one email address per row, no header required. Max file size: 10 MB. Max rows: 500,000.
Response (202 Accepted)
{
"rosterVersionId": "a1b2c3d4-...",
"total": 1530,
"status": "processing"
}Use the rosterVersionId to poll for completion via the upload-status endpoint.
/api/upload-status
MembershipPoll the status of a roster upload.
Request
curl https://tryperkspring.com/api/upload-status?versionId=a1b2c3d4-... \ -H "Authorization: Bearer pk_your_api_key"
Response
{
"id": "a1b2c3d4-...",
"status": "active",
"totalRows": 1530,
"processedRows": 1530,
"error": null,
"createdAt": "2026-06-19T...",
"activatedAt": "2026-06-19T..."
}Status values: processing, active, failed, archived.
/api/redemptions/{code}
RetailerLook up a redemption code presented at checkout. Returns the discount you agreed to honor, frozen at the moment the code was issued, so later edits to the offer never change what a shopper was promised. This does not consume the code.
Response
{
"valid": true,
"code": "PS-5018F6E54445AB7D",
"status": "issued",
"perk_name": "Military Appreciation 15%",
"verified_via": "Armed Forces Alliance",
"discount": {
"type": "percent_off",
"percent_off": 15,
"amount_off_cents": null,
"min_subtotal_cents": 5000,
"max_discount_cents": 10000,
"currency": "USD"
},
"transaction_ref": "order_10482",
"issued_at": "2026-09-15T18:20:11Z",
"expires_at": "2026-09-15T18:50:11Z",
"consumed_at": null
}When a code cannot be honored the response carries valid: false and a reason of not_found, expired, or already_consumed. A code belonging to another retailer returns 404 — codes are never visible across organizations.
/api/redemptions/{code}
RetailerMark a code consumed once the order is placed. Call this after the discount has actually been applied. The update is conditional, so two concurrent checkouts cannot both consume the same code.
Response
{
"valid": true,
"code": "PS-5018F6E54445AB7D",
"status": "consumed",
"consumed_at": "2026-09-15T18:31:02Z"
}A second attempt returns 409 with reason: "already_consumed". An expired code returns 409 with reason: "expired" and is never consumed.
/api/erasure
MembershipRemove a member's data from the platform (GDPR Article 17 right to erasure). Deletes all matching hashes and redacts verification history.
Request
{
"email": "[email protected]"
}Response
{
"erased": true,
"hashesRemoved": 1
}All erasure requests are logged in an immutable audit trail for compliance.
/api/orgs
RetailerList membership organizations available for verification. Returns only orgs with an active partnership to your organization.
Response
{
"organizations": [
{
"id": "5739777b-8a89-417d-...",
"name": "AAA Northeast",
"memberCount": 12450,
"createdAt": "2026-06-18T07:42:56Z"
}
]
}/api/usage
Get verification usage metrics for your organization.
Response
{
"total": 4820,
"thisMonth": 1230,
"prevMonth": 980,
"daily": [
{ "date": "2026-09-01", "count": 45 },
{ "date": "2026-09-02", "count": 62 }
]
}/api/keys
Create a new API key for your organization.
Request
{
"label": "production",
"environment": "live"
}Response
{
"key": "pk_a1b2c3d4...",
"label": "production",
"environment": "live",
"note": "Store this key securely — it cannot be retrieved again."
}/api/keys
List all API keys for your organization. Key values are not returned — only metadata (label, environment, status, creation date).
/api/keys
Revoke or reactivate an API key.
Request
{
"keyId": "a1b2c3d4-...",
"active": false
}Response
{
"keyId": "a1b2c3d4-...",
"active": false
}Revoked keys stop authenticating immediately.
/api/partnerships
List all partnerships for your organization, with partner name and type.
/api/partnerships
Create a new partnership. Must be between a retailer and a membership organization.
Request
{
"partnerOrgId": "b804f10e-3300-4670-..."
}/api/partnerships
Activate or deactivate a partnership. Either party can change the status.
Request
{
"partnershipId": "c905a21f-4411-5781-...",
"status": "inactive"
}/api/health
Health check endpoint. No authentication required.
Response
{ "status": "ok" }Returns 503 with {"status": "degraded", "db": "unreachable"} if the database is down.
Error Codes
| Code | Meaning |
|---|---|
| 200 | OK — request succeeded |
| 202 | Accepted — upload is processing in the background |
| 400 | Bad Request — missing or invalid parameters |
| 401 | Unauthorized — missing or invalid API key |
| 403 | Forbidden — wrong org type for this endpoint |
| 404 | Not Found — resource doesn't exist |
| 409 | Conflict — duplicate (partnership, upload in progress, or idempotency key collision) |
| 413 | Payload Too Large — file exceeds 10 MB or 500K rows |
| 429 | Too Many Requests — rate limit exceeded |
| 500 | Internal Server Error |