# PlatinumCard.ai POST https://validate.platinumcard.ai to check that a card-portfolio declaration is well-formed. The call works across issuers and is non-custodial. It does not choose a card for a payment, and it does not evaluate a payment. PlatinumCard.ai is an independent software protocol. It is not a bank, is not affiliated with American Express, Visa, Mastercard, or any individual card issuer, and does not issue credit products. ## One-line summary Validate checks the declaration: version, cards, networks, statuses, MCC spelling, and secret-like field names. Rank (partner, keyed) recommends a card for a specific transaction. ## Example POST a portfolio. Limits and MCC lists in this example are recorded on the declaration. This endpoint checks that the document is well-formed. It does not apply those fields to a charge. ```json { "version": "0.1.1", "owner": "demo-agent", "cards": [ { "id": "amex-plat", "network": "amex", "product_tier": "platinum", "issuer": "American Express", "product_name": "The Platinum Card", "status": "active", "constraints": { "per_transaction_limit": 1200, "allowed_mccs": ["3000-3299", "4511", "5812"], "require_human_approval": true, "approval_threshold": 400 } }, { "id": "chase-reserve", "network": "visa", "product_tier": "infinite", "issuer": "Chase", "product_name": "Sapphire Reserve", "status": "active", "constraints": { "per_transaction_limit": 2000, "allowed_mccs": ["3000-3299", "4511", "5812"] } } ] } ``` Response when the declaration is well-formed: ```json { "valid": true, "errors": [], "warnings": [], "card_count": 2, "schema_version": "0.1.1", "disclaimer": "PlatinumCard.ai is an independent software protocol. It is not a bank, is not affiliated with American Express, Visa, Mastercard, or any individual card issuer, and does not issue credit products." } ``` A duplicate card id is rejected. This body: ```json { "version": "0.1.1", "cards": [ { "id": "card1", "network": "visa", "status": "active" }, { "id": "card1", "network": "amex", "status": "active" } ] } ``` returns: ```json { "valid": false, "errors": [ { "path": "cards.1.id", "message": "Duplicate card id", "code": "DUPLICATE_CARD_ID" } ], "warnings": [], "card_count": 2, "schema_version": "0.1.1", "disclaimer": "PlatinumCard.ai is an independent software protocol. It is not a bank, is not affiliated with American Express, Visa, Mastercard, or any individual card issuer, and does not issue credit products." } ``` ## Where it fits Use Validate when you build or change a portfolio: at startup, or in CI, to catch a malformed declaration early. It is not a payment check. A valid result means the document is well-formed. It does not mean a card can be used for an amount or a merchant. ```python import requests def declaration_is_well_formed(portfolio): response = requests.post( "https://validate.platinumcard.ai", json=portfolio, timeout=10, ) result = response.json() return result["valid"], result["errors"] ``` ```javascript async function declarationIsWellFormed(portfolio) { const response = await fetch("https://validate.platinumcard.ai", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(portfolio), }); const result = await response.json(); return [result.valid, result.errors]; } ``` To check a payment before it is made, use the partner rank service (below). It takes the portfolio and the transaction and checks the transaction against the limits and lists you recorded. Rank advises: your code has to honour the answer. That call takes an API key. ## Schema Portfolio JSON Schema: https://platinumcard.ai/schema/portfolio-v0.1.1.json The schema matches the checks this endpoint performs. Each spend limit and the approval threshold must be a number >= 0 or null (INVALID_LIMIT), and `constraints` accepts only the keys the schema lists (INVALID_FIELD), so a misspelled limit is caught instead of silently ignored. The endpoint does not compare limits to a payment. Merchant lists, country lists, preferences, and optimization policy are recorded fields. The endpoint does not evaluate them. It does reject an inverted MCC range (INVALID_MCC_RANGE) and any property whose name is on the secret-key denylist, at any depth, compared case-insensitively. ## How to call Endpoint: `https://validate.platinumcard.ai` Method: `POST` Content-Type: `application/json` Body size limit: 256 KiB Rate limit: 60/min per IP Stability: `unstable` (schema version 0.1.1) GET / returns service metadata as JSON. The Accept header does not change that body. GET /llms.txt and GET /docs return this document. No authentication is required. Validation is free and public. Status codes from POST: - `200`: the declaration is well-formed (`"valid": true`). - `422`: the declaration was read and is not valid (`"valid": false`, with `errors`). The duplicate-id example above returns 422. - `400`: the body is not JSON. - `415`: the Content-Type is not `application/json`. - `413`: the body is over 256 KiB. - `405`: the method is not allowed (only GET and POST are served). - `429`: more than 60 requests per minute from one IP (see the `retry-after` header). Check `valid` in the body, not only the HTTP status. ## Availability and data Service level: none. Validate and Rank are offered on a best-efforts basis, with no uptime or latency commitment. Status: `GET https://rank.platinumcard.ai/rank` needs no key and shows the registry snapshot and its age. `GET https://validate.platinumcard.ai/` shows the validate service status. Failure: Rank fails closed. Treat any non-200 as "do not pay on this answer". It also refuses with 503 when its registry snapshot is more than 90 days old. Data: Validate does not store what you send. For Rank, the usage log pc-ai keeps for each request holds the request id, organization id, key id, time, outcome, HTTP status, route, registry snapshot and key environment. It does not hold the portfolio, the transaction amount, MCC, merchant or country. The service runs on Cloudflare, which keeps its own platform logs. ## Partner rank service Validate checks the declaration. Rank applies it to one transaction. Rank is a partner feature: rank.platinumcard.ai requires an API key (`Authorization: Bearer `). Start with a free live pilot (60 days, 2,000 ranks included) or a 30-day test key. After the pilot, plans start at $299 per month with 10,000 ranks included. To request either, email team@platinumcard.ai with your company and what your agent buys. Test keys last 30 days, never bill, and mark responses `"env": "test"`. Reply to the same email to extend one while you are integrating. Rate limits: 6 requests per minute on a test key, 60 per minute on a live key, counted per organization. Send `POST https://rank.platinumcard.ai/rank` (HTTPS, `Content-Type: application/json`) with the portfolio, a transaction (amount is required; mcc, merchant, country and currency are optional) and, to compare cards that earn different points, `point_values`: what one point is worth to you, in cents, for each program. pc-ai does not value points itself: ```json { "portfolio": { "version": "0.1.1", "cards": [ "...the two cards from the example above..." ] }, "transaction": { "amount": 1500, "mcc": "4511", "merchant": "Example Air", "country": "US", "currency": "USD" }, "point_values": { "amex_mr": 1.2, "chase_ur": 1.25 } } ``` Rank checks each card's recorded constraints against the transaction (status, validity window, MCC, merchant and country lists, approval threshold, and the per-transaction, daily, monthly and lifetime limits), then ranks the cards that remain. Each limit is compared with this one transaction's amount. Rank does not track running spend, so a daily or monthly limit stops a single charge larger than the limit, not a total across charges. Keep your own running totals if you need them. `amount` is a plain number compared directly with the limits you recorded, so use the same units for both. Rank does no currency conversion. A 1500 USD airline charge on the example portfolio returns (fields as returned, disclaimer and env omitted here): ```json { "recommendation": { "card_id": "chase-reserve", "score": 5, "requires_approval": false, "reasons": ["4x flights direct (MCC 4511), 5¢ per $1 at 1.25¢ per chase_ur point", "travel credit may apply (not scored)", "lounge access (not scored)"], "registry_id": "chase-sapphire-reserve", "program": "chase_ur", "earn_rate": 4, "value_cents_per_dollar": 5 }, "alternatives": [], "filtered_out": [{ "card_id": "amex-plat", "reason": "EXCEEDS_PER_TRANSACTION_LIMIT" }], "reasoning": "Selected chase-reserve: 4x flights direct (MCC 4511), 5¢ per $1 at 1.25¢ per chase_ur point, travel credit may apply (not scored), lounge access (not scored).", "registry_snapshot": "seed-2026-10", "warnings": [] } ``` Rank values earn as rate × your cents per point. A category counts only when its MCC list matches the transaction; a rate that depends on a booking portal or one merchant brand is not credited to a plain swipe. With no category match, the card's base rate applies. Credits and lounge access are listed as "(not scored)", and a capped rate is flagged ("capped: … then base rate"), because rank can't see what you have already used or spent. When eligible cards earn in different programs and `point_values` does not value them all, there is no single pick: `recommendation` is `null`, `by_program` lists the best card in each program, and `warnings` holds `MISSING_POINT_VALUES`. A 300 USD airline charge on the example portfolio without `point_values` returns `amex-plat` (`amex_mr`, 5x) and `chase-reserve` (`chase_ur`, 4x) in `by_program`. Programs in the current registry: `amex_mr`, `chase_ur`, `citi_thankyou`, `capital_one_miles`, `boa_points`, `wells_fargo_rewards`, `atmos_rewards`, `delta_skymiles`, `united_mileageplus`, `hilton_honors`, `marriott_bonvoy`. When no card is eligible, `recommendation` is `null`, HTTP status is still 200, and `filtered_out` names the reason for each card. A 90 USD charge at MCC 5999 on the same portfolio returns `MCC_NOT_ALLOWED` for both cards. A card that needs a human returns `"requires_approval": true`. `at` (an ISO 8601 timestamp, default now) is checked against each card's `valid_from` and `valid_until`. `currency` is accepted and not used. Rank's response fields: - `recommendation`: the best eligible card (`card_id`, `score`, `requires_approval`, `reasons`, `registry_id`, `program`, `earn_rate`, `value_cents_per_dollar`), or `null` when none is eligible or there is no common value. `score` is cents per $1 when valued, otherwise the earn rate in the card's own points. - `by_program`: only when there is no single pick; the best card in each program, same shape. - `alternatives`: up to two more eligible cards, same shape. - `filtered_out`: each card that was ruled out, with `card_id` and a `reason`: `CARD_NOT_ACTIVE`, `OUTSIDE_VALID_WINDOW`, `EXCEEDS_PER_TRANSACTION_LIMIT`, `EXCEEDS_DAILY_LIMIT`, `EXCEEDS_MONTHLY_LIMIT`, `EXCEEDS_LIFETIME_LIMIT`, `MCC_BLOCKED`, `MCC_NOT_ALLOWED`, `COUNTRY_BLOCKED`, `COUNTRY_NOT_ALLOWED`, `MERCHANT_BLOCKED` or `MERCHANT_NOT_ALLOWED`. - `reasoning`, `registry_snapshot`, `warnings`, `disclaimer`, and `env` (`"test"` or `"live"`). Rank errors have the shape `{"error": {"code": "...", "message": "..."}, "disclaimer": "..."}`: | Code | HTTP | Meaning | |---|---|---| | `MISSING_CREDENTIAL` | 401 | No `Authorization` header, or an empty Bearer | | `INVALID_KEY` | 401 | Malformed, unknown, revoked or expired key (one code on purpose) | | `ORG_MISMATCH` | 403 | `X-PC-AI-Org` is set and does not match the key's organization | | `INVALID_REQUEST` | 400 | The body is not a JSON object | | `MISSING_AMOUNT` | 400 | The transaction has no amount | | `INVALID_PORTFOLIO` | 422 | The portfolio failed validation; `validation.errors` has the same errors Validate returns, and nothing is ranked | | `UNSUPPORTED_MEDIA_TYPE` | 415 | `Content-Type` is not `application/json` | | `PAYLOAD_TOO_LARGE` | 413 | The body is over 256 KiB | | `METHOD_NOT_ALLOWED` | 405 / 404 | 405 for a method other than `GET` or `POST`; 404 for a `POST` to an unknown path | | `RATE_LIMITED` | 429 | Over the key's rate limit (6 per minute test, 60 per minute live) | | `KEY_STORE_UNAVAILABLE` | 503 | Rank cannot check keys right now and fails closed | | `REGISTRY_STALE` | 503 | The card registry snapshot is past its 90-day limit | | `INTERNAL_ERROR` | 503 / 500 | Something broke on our side | A `GET` to rank.platinumcard.ai needs no key and returns a status document (service, `registry_snapshot`, `registry_age_days`, `registry_max_age_days`, `docs`). It is not a rank. The status document on validate.platinumcard.ai also carries `docs`, a link to this document. Rank refuses instead of answering when its card registry snapshot is more than 90 days old (HTTP 503, `REGISTRY_STALE`). Treat any non-200 as "do not pay on this answer". Validation is free. Ranking is keyed. ## What this is not - Not a bank - Not an issuer - Not a lender - Not a payment processor - No funds held, no accounts opened, no credit extended - Not affiliated with American Express, Visa, Mastercard, Discover, or any card issuer