Guide
MINT API — Quickstart
Read-only REST API over the CRM messages that Brazilian banks and fintechs send to their customers, as catalogued in MINT by XCORP.
- Base URL:
https://api.mint.xcorp.cc/v1 - Only
GET. JSON in UTF-8. - Reference:
GET /v1/openapi.json(OpenAPI 3.1). Guide for AI agents:GET /v1/guide.
What the API provides
| Available | Not in this version |
|---|---|
| Messages of the last 30 days (rolling window), CRM Brazil | History older than 30 days |
Counts over 13 months in /v1/stats (by day, institution, tag, product, channel) | MCP server, bulk export, webhooks |
| Filters and full-text search inside the window | Semantic search |
The full creative image of each message (image/webp) | Original or other image sizes |
| Yesterday's messages available by 4 p.m. Brasília time | SLA; support is during business hours (Brasília) |
There is no send time. A message has a date (YYYY-MM-DD, Brasília) and a seq, its order within the day for that institution. Sort by date, institution, seq if you need an order.
1. Credentials
XCORP gives you one service token per agent: a Client ID and a Client Secret. Send both on every request:
CF-Access-Client-Id: <client id>
CF-Access-Client-Secret: <client secret>Requests are also restricted to your registered outbound IPs. Keep the secret in a secret store, never in code, prompts or logs. If it leaks, tell XCORP and the token is revoked.
export MINT_ID='<client id>'
export MINT_SECRET='<client secret>'2. curl
Check the service (no data, free):
curl -s https://api.mint.xcorp.cc/v1/health -H "CF-Access-Client-Id: $MINT_ID" -H "CF-Access-Client-Secret: $MINT_SECRET"{"ok": true, "as_of": "2026-11-08"}as_of is the date of the last completed data update.
What was sent since yesterday (the main use case; since defaults to yesterday):
curl -s "https://api.mint.xcorp.cc/v1/feed?insts=nubank,itau" -H "CF-Access-Client-Id: $MINT_ID" -H "CF-Access-Client-Secret: $MINT_SECRET"{
"data": [
{
"id": "9b1e4c7a2d5f8e0b3a6c9d2e5f8a1b4c",
"institution": "nubank",
"date": "2026-11-08",
"seq": 3,
"title": "Ganhe cashback no Pix no crédito",
"channel": "push",
"tags": ["Cashback", "Pix"],
"summary": "Offers 2% cashback on Pix paid with the credit card until month end."
}
],
"next_cursor": null,
"as_of": "2026-11-08"
}Search (full text, accent-insensitive, inside the window):
curl -s "https://api.mint.xcorp.cc/v1/messages?q=cashback&channels=push,email&from=2026-11-01" -H "CF-Access-Client-Id: $MINT_ID" -H "CF-Access-Client-Secret: $MINT_SECRET"One message with all fields, and its image:
curl -s https://api.mint.xcorp.cc/v1/messages/9b1e4c7a2d5f8e0b3a6c9d2e5f8a1b4c -H "CF-Access-Client-Id: $MINT_ID" -H "CF-Access-Client-Secret: $MINT_SECRET"curl -s -o cover.webp https://api.mint.xcorp.cc/v1/messages/9b1e4c7a2d5f8e0b3a6c9d2e5f8a1b4c/images/0 -H "CF-Access-Client-Id: $MINT_ID" -H "CF-Access-Client-Secret: $MINT_SECRET"Trend over 13 months, without spending budget:
curl -s "https://api.mint.xcorp.cc/v1/stats?by=day&from=2025-11-01&insts=nubank" -H "CF-Access-Client-Id: $MINT_ID" -H "CF-Access-Client-Secret: $MINT_SECRET"Your consumption:
curl -s https://api.mint.xcorp.cc/v1/usage -H "CF-Access-Client-Id: $MINT_ID" -H "CF-Access-Client-Secret: $MINT_SECRET"3. Python (standard library only)
import json, os, time, urllib.error, urllib.parse, urllib.request
BASE = "https://api.mint.xcorp.cc/v1"
HEADERS = {
"CF-Access-Client-Id": os.environ["MINT_ID"],
"CF-Access-Client-Secret": os.environ["MINT_SECRET"],
"User-Agent": "my-agent/1.0",
}
def get(path, **params):
url = BASE + path + ("?" + urllib.parse.urlencode(params) if params else "")
for attempt in range(5):
try:
with urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS)) as r:
return json.load(r), r.headers
except urllib.error.HTTPError as e:
try:
body = json.load(e)
except ValueError: # rejected before reaching the API: check credential and IP
body = {"error": "access", "message": "non-JSON response"}
if e.code == 429 and (body.get("detail") or {}).get("limit") == "minute":
time.sleep(int(e.headers.get("retry-after", "60")))
continue
raise RuntimeError(f"{e.code} {body['error']}: {body['message']}")
raise RuntimeError("still rate limited")
def feed(since=None, **filters):
"""All pages of /v1/feed. Same parameters on every page, plus the cursor."""
params = {k: v for k, v in {"since": since, **filters}.items() if v}
while True:
page, headers = get("/feed", **params)
print("charged now:", headers["x-records-new"], "budget left:", headers["x-budget-remaining"])
yield from page["data"]
if not page["next_cursor"]:
return
params["cursor"] = page["next_cursor"]
for card in feed(insts="nubank,itau"):
print(card["date"], card["seq"], card["institution"], card["title"])4. Pagination
Lists return up to limit items (1 to 50, default 50) and a next_cursor. To get the next page, repeat the same filters and dates and add cursor=<next_cursor> (limit and view may change). A cursor is valid for 1 hour, only for your credential, those filters and the dates they resolved to: if you rely on the default since/from, a day change in Brasília invalidates it. Otherwise you get 400 bad_cursor and should restart without a cursor. next_cursor is null on the last page.
A to after today is treated as today; from after to returns an empty list. Unknown or repeated parameters return 400 bad_request with detail = {param, why}; /v1/messages/{id} accepts no parameters (it is always the full view).
5. Budget and limits
Your access plan sets the limits below and the end date of your access; GET /v1/usage shows them.
- Budget: each distinct message returned in a response (card or full) counts once per calendar month. Returning it again in the same month costs nothing. 5,000 distinct messages per month.
/v1/stats, catalogs,/v1/usage,/v1/healthand images do not count. - Every response carries
x-records-new(charged by this response; 0 on routes that do not charge) andx-budget-remaining(left this month).GET /v1/usageshows the totals; itsrequests_todayalready includes that request. - Every route, including
/v1/health,/v1/openapi.jsonand/v1/guide, needs the credential and counts towards the daily request limit. - Limits: 60 requests per minute, 5,000 requests per day, 1,000 images per day. Over a limit:
429 rate_limitedwith aretry-afterheader (seconds) anddetail.limit=minute,day,imagesorbudget. - Access has an end date (
ends_onin/v1/usage). After it, requests return403 access_ended.
6. Errors
All errors are {"error": "<code>", "message": "<what to do>", "detail": ...}.
| Status | error | Meaning |
|---|---|---|
| 400 | bad_request | Invalid, unknown or repeated parameter; detail = {param, why} |
| 400 | window | Date before the 30-day window; detail has the first accepted date. Use /v1/stats for older trends |
| 400 | bad_cursor | Cursor tampered, expired, from another credential or other filters |
| 403 | forbidden | Missing or unknown credential |
| 403 | disabled / access_ended | Your access is disabled or its period has ended. Do not retry; contact XCORP |
| 404 | not_found | Message missing, out of the window or not visible to you; or unknown slug or id in a filter (all look the same) |
| 405 | method_not_allowed | Only GET (HEAD and OPTIONS too return 405) |
| 429 | rate_limited | See limits above |
| 500 | internal | Unexpected error; retry later |
| 503 | disabled | Service temporarily disabled for everyone; retry after retry-after seconds |
Quote the x-request-id header when contacting support.