LimePay API v1 (read-only)
Base URL: https://limepay.app/api/v1
Auth
Authenticate every request with a Bearer token generated in Settings → API keys:
Authorization: Bearer lpk_live_abcd1234ef567890.deadbeef…(32 hex chars)
Tokens come in two modes:
lpk_live_…— acts on production datalpk_test_…— reserved for sandbox (not yet wired to a separate dataset; same DB, mode flag preserved for forward compat)
Revoke keys you no longer need from the same settings page — revocation is immediate.
Errors
JSON-encoded:
{ "error": { "code": "unauthorized", "message": "Invalid API key" } }
| HTTP | code | meaning |
|---|---|---|
| 401 | unauthorized | Missing / malformed / revoked / invalid key |
| 404 | not_found | The resource doesn't exist within your vendor scope |
| 400 | various | Bad request / validation failure |
| 500 | server_error | DB or upstream issue |
Vendor scope is enforced server-side from the API key. You can never see another vendor's data.
Pagination
List endpoints accept:
limit— default 50, max 100offset— default 0
Responses include:
{
"data": [ … ],
"pagination": {
"total": 137,
"limit": 50,
"offset": 0,
"has_more": true
}
}
Endpoints
GET /v1/subscriptions
List subscriptions belonging to the authed vendor. Returns the row plus FK references to plan and subscriber (so you can fetch them in follow-up calls if needed).
Query params
status—active|pending|paused|past_due|cancelledsubscriber_id— UUID; only subs for a specific buyerplan_id— UUID; only subs for a specific planlimit/offset— see pagination
Example
curl https://limepay.app/api/v1/subscriptions?status=active \
-H "Authorization: Bearer lpk_live_…"
Response (truncated)
{
"data": [
{
"id": "30268041-69e2-4862-9f9f-276ce1fac517",
"status": "active",
"plan_id": "0610f789-aea6-4c61-8c88-9859d11392d1",
"subscriber_id": "076c4fbc-65ff-45d3-b47f-258f108e5d47",
"mp_preapproval_id": "5cef672e85cc466ea6d2eceb3e75ccec",
"started_at": "2026-05-12T20:24:21.939Z",
"current_period_start": "2026-05-12T20:24:21.939Z",
"current_period_end": "2026-06-12T20:24:22Z",
"cancel_at_period_end": false,
"cancelled_at": null,
"created_at": "2026-05-12T20:41:13.265Z"
}
],
"pagination": { "total": 1, "limit": 50, "offset": 0, "has_more": false }
}
GET /v1/subscriptions/{id}
Fetch a single subscription. Returns 404 if the id doesn't belong to your vendor (we don't distinguish "doesn't exist" from "doesn't belong to you" to avoid leaking existence).
Example
curl https://limepay.app/api/v1/subscriptions/30268041-69e2-4862-9f9f-276ce1fac517 \
-H "Authorization: Bearer lpk_live_…"
POST /v1/subscriptions/{id}/cancel
Cancel a subscription. Idempotent — calling it on an already-cancelled subscription returns 200 with the current state, not an error.
Cancellation propagates to Mercado Pago (PUT /preapproval/{id} with status: "cancelled"), updates the local row to status: "cancelled" with cancelled_at set, and fans out a subscription.cancelled webhook to your registered endpoints (via the inbound MP reconciliation webhook — usually within a few seconds).
Body (optional, JSON):
| Field | Type | Notes |
|---|---|---|
reason | string, ≤ 500 chars | Free text. Surfaced in the dashboard's churn view. |
Errors
| Status | Code | When |
|---|---|---|
404 | not_found | Subscription not found for this vendor. |
409 | invalid_state | Subscription isn't linked to MP yet (still pending). |
502 | upstream_error | Mercado Pago rejected the cancellation. |
Example
curl -X POST https://limepay.app/api/v1/subscriptions/30268041-69e2-4862-9f9f-276ce1fac517/cancel \
-H "Authorization: Bearer lpk_live_…" \
-H "Content-Type: application/json" \
-d '{"reason": "customer requested via support ticket #4821"}'
Returns the updated subscription:
{
"data": {
"id": "30268041-69e2-4862-9f9f-276ce1fac517",
"status": "cancelled",
"cancelled_at": "2026-05-13T18:42:11.000Z",
"..."
}
}
GET /v1/subscribers
List subscribers belonging to the vendor.
Query params
email— exact match; typical use case: "is this customer in LimePay?"limit/offset— see pagination
Example
curl "https://limepay.app/api/v1/subscribers?email=cafetero@gmail.com" \
-H "Authorization: Bearer lpk_live_…"
GET /v1/subscribers/{id}
Fetch a single subscriber.
curl https://limepay.app/api/v1/subscribers/076c4fbc-65ff-45d3-b47f-258f108e5d47 \
-H "Authorization: Bearer lpk_live_…"
Typical fulfillment flow
# 1. On dispatch day, get all active subscribers
curl "https://limepay.app/api/v1/subscriptions?status=active&limit=100" \
-H "Authorization: Bearer $LIMEPAY_KEY" \
| jq -r '.data[].subscriber_id' \
| while read sub_id; do
curl -s "https://limepay.app/api/v1/subscribers/$sub_id" \
-H "Authorization: Bearer $LIMEPAY_KEY" \
| jq '{email, name}'
done
Outbound webhooks
Configure under Settings → Webhooks in the dashboard. We POST a signed JSON body to your URL whenever a lifecycle event happens on your vendor.
Delivery
POST https://your-app.com/webhooks/limepay
Content-Type: application/json
User-Agent: LimePay-Webhooks/1
X-LimePay-Event-Id: evt_…
X-LimePay-Event-Type: subscription.cancelled
X-LimePay-Signature: t=1715625600,v1=<sha256-hex>
{
"id": "evt_…",
"type": "subscription.cancelled",
"api_version": "2026-05-13",
"created": 1715625600,
"data": {
"subscription_id": "…",
"mp_preapproval_id": "…",
"status": "cancelled",
"previous_status": "active",
"plan_id": "…",
"subscriber_id": "…"
}
}
Verifying the signature
v1 = HMAC_SHA256(secret, "<t>.<rawBody>") where secret is the value we showed you once at creation time (prefixed whsec_…).
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(
header.split(",").map((p) => {
const eq = p.indexOf("=");
return [p.slice(0, eq), p.slice(eq + 1)];
}),
);
const t = Number(parts.t);
if (!Number.isFinite(t)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false; // 5-min replay window
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const got = Buffer.from(parts.v1 ?? "", "hex");
return expected.length === got.length && timingSafeEqual(expected, got);
}
Replay protection: reject if the t= timestamp is more than 5 minutes off (we send the current time).
Retries and reliability
- 10-second request timeout.
- Any non-2xx response (or network failure) is retried by Inngest on 1m / 5m / 30m / 2h / 6h backoff (5 retries total).
- 5 consecutive failures on the same endpoint → we auto-disable the webhook and surface it in the dashboard so the vendor can fix and reactivate.
- Every attempt is logged in
webhook_deliveriesand visible in Settings → Webhooks → Últimas entregas.
Idempotency
The X-LimePay-Event-Id header is stable for a given logical event. If we retry, we re-send it. Dedupe on your side by storing seen event ids — your processing should be idempotent.
Events
| Event | When |
|---|---|
subscription.activated | New sub or trial just turned active |
subscription.cancelled | Buyer or vendor cancelled |
subscription.paused | Sub set to paused |
subscription.past_due | MP flagged a recurring payment as past due |
subscription.recovered | Came back to active from paused/past_due |
subscription.renewed | (reserved — fired on each successful renewal cobro, soon) |
payment.succeeded | MP approved a charge for > $0 |
payment.failed | MP rejected a charge |
invoice.issued | ARCA issued the fiscal invoice with CAE |
Payload shapes
subscription.*:
{
"subscription_id": "uuid",
"mp_preapproval_id": "string",
"status": "active|cancelled|paused|past_due",
"previous_status": "active|cancelled|paused|past_due|null",
"plan_id": "uuid",
"subscriber_id": "uuid",
"started_at": "ISO-8601 | null",
"current_period_end": "ISO-8601 | null"
}
payment.succeeded / payment.failed:
{
"transaction_id": "uuid",
"mp_payment_id": "string",
"subscription_id": "uuid | null",
"amount": 10000,
"currency": "ARS",
"paid_at": "ISO-8601 | null",
"reason": "string | null" // only on payment.failed
}
invoice.issued:
{
"invoice_id": "uuid",
"transaction_id": "uuid",
"subscriber_id": "uuid",
"tipo_comprobante": 11,
"punto_venta": 2,
"numero": 5,
"cae": "86195303445754",
"total": 10000
}
Implementing access control
If your product gates content/features behind an active subscription, the rule is simple: grant access while status === "active", revoke it otherwise. LimePay sends you every state transition as a webhook, so you don't have to poll.
The four events that matter for gating:
| Event | What it means | What to do |
|---|---|---|
subscription.activated | New sub paid + active | Grant access |
subscription.past_due | Last cobro failed (incl. first cobro) — MP will retry | Revoke access immediately if you don't want to extend credit; or keep it open until cancelled if you're more lenient |
subscription.recovered | Sub came back to active (buyer fixed the card / MP retry succeeded) | Restore access |
subscription.cancelled | Sub closed for good | Revoke access permanently |
Why past_due fires on a first failed cobro
MP marks the preapproval authorized as soon as the card validates, so subscriptions land as active before any money has actually cleared. If the very first charge attempt is rejected (insufficient funds, declined, etc.), LimePay demotes the sub to past_due and fires subscription.past_due — even though no successful cobro ever happened. Treat it identically to a later failure: the buyer hasn't paid.
Recommended handler
def handle_limepay_webhook(event_type, payload):
sub_id = payload["subscription_id"]
subscriber_id = payload["subscriber_id"]
user = User.objects.get(limepay_subscriber_id=subscriber_id)
if event_type == "subscription.activated":
user.is_paid_member = True
user.save()
elif event_type in ("subscription.past_due", "subscription.cancelled"):
# No grace period: cut access on the first rejected cobro.
user.is_paid_member = False
user.save()
elif event_type == "subscription.recovered":
user.is_paid_member = True
user.save()
You decide the policy. Some vendors give a grace window between past_due and cancelled (MP retries for ~10 days before giving up); others revoke immediately on past_due because they don't want to extend service to non-payers. Both are fine — LimePay just delivers the signals.
Mapping subscriber → user
Link your user to the LimePay subscriber once, on subscription.activated. The payload has subscriber_id (LimePay's id) and you can fetch the buyer's email/document via GET /v1/subscribers/{id} if you don't have it yet. From then on, every webhook for that sub references the same subscriber_id.
If you set external_id on the subscriber at checkout time (via the buyer-facing form), it round-trips on every subscriber payload — easier than maintaining a join table.
Don't poll
You technically can hit GET /v1/subscriptions/{id} and read status, but you'll be late: webhooks fire within seconds of the MP event landing on our side, while polling adds whatever interval you chose. Use the API for backfills / reconciliation, not for primary signal.
Roadmap
- More write endpoints (pause/resume, refund payment, etc.) — when there's demand from a real vendor.
GET /v1/invoices— same scope rules, returns issued ARCA invoices with CAE + PDF URL.GET /v1/plans+POST /v1/plans— already covered by the dashboard for now.subscription.renewedevent — fires on each successful renewal cobro (currentlypayment.succeededcovers this case).