# MexicoP2P Partner API — Full Documentation > Generated from docs.mexicop2p.org MDX source files. > For the concise version, see /llms.txt ============================================================ ## "GET /exchange-rate" Source: api-reference/exchange-rate.mdx ============================================================ ## GET /exchange-rate Returns the current USD/MXN exchange rate sourced from Banxico (primary) or CoinGecko (fallback). ### Request ```bash curl -H "X-API-Key: mp2p_test_xxx" \ https://mexicop2p.org/api/v1/exchange-rate ``` ### Response `200` ```json { "usdMxn": 17.25, "source": "banxico", "timestamp": "2025-06-15T12:00:00.000Z", "validFor": 300 } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `usdMxn` | number | Current USD to MXN rate | | `source` | string | Rate provider: `"banxico"` or `"coingecko"` | | `timestamp` | string | When the rate was fetched | | `validFor` | number | Seconds this rate is valid (300 = 5 minutes) | ### Usage notes - Cache this response for `validFor` seconds to reduce API calls - Use `POST /quotes` to lock a rate for a specific trade - The rate shown here is the mid-market rate; the actual trade rate is set when creating a quote ============================================================ ## "GET /health" Source: api-reference/health.mdx ============================================================ ## GET /health Verify your API key is valid and check your partner account status. ### Request ```bash curl -H "X-API-Key: mp2p_test_xxx" \ https://mexicop2p.org/api/v1/health ``` ### Response `200` ```json { "status": "ok", "partner": "Acme Finance", "tier": "GROWTH", "timestamp": "2025-06-15T12:00:00.000Z" } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `status` | string | Always `"ok"` if authenticated | | `partner` | string | Your partner display name | | `tier` | string | `FREE`, `GROWTH`, or `ENTERPRISE` | | `timestamp` | string | ISO 8601 server timestamp | ### Errors | Code | HTTP | When | |------|------|------| | `MISSING_API_KEY` | 401 | No `X-API-Key` header | | `INVALID_API_KEY` | 401 | Key doesn't match any partner | | `PARTNER_INACTIVE` | 403 | Account not active | ============================================================ ## "GET /orders/:id" Source: api-reference/order-detail.mdx ============================================================ ## GET /orders/:id Retrieve complete details for a single order, including calculated spread, escrow status, bank details, and event timeline. ### Request ```bash curl -H "X-API-Key: mp2p_test_xxx" \ https://mexicop2p.org/api/v1/orders/ord_xyz789 ``` ### Response `200` ```json { "id": "ord_xyz789", "escrowOrderId": "0x1a2b3c4d5e6f...", "status": "COMPLETED", "token": "USDC", "amount": 100, "amountRaw": "100000000", "pricePerUnit": 17.35, "totalMxn": 1735.00, "fxRateUsdMxn": 17.25, "fxRateSource": "partner_quote", "spread": { "percent": 0.58, "mxn": 10.00 }, "escrowTxHash": "0xabc123...", "escrowStatus": "CONFIRMED", "releaseTxHash": "0xdef456...", "buyerWallet": "0x04a1b2c3...", "lockedAt": "2025-06-15T12:15:00.000Z", "lockExpiresAt": "2025-06-16T12:15:00.000Z", "bankAccount": { "bankName": "BBVA México", "bankCode": "012", "clabe": "012345678901234567" }, "cepValidatedAt": "2025-06-15T12:30:00.000Z", "cepTrackingKey": "MXBA2025061512300001", "amlRiskScore": 12, "amlRiskLevel": "LOW", "partnerUserRef": "user_12345", "createdAt": "2025-06-15T12:03:00.000Z", "completedAt": "2025-06-15T12:35:00.000Z", "expiresAt": "2025-06-15T13:03:00.000Z", "timeline": [ { "id": "evt_001", "event": "CREATED", "data": {}, "timestamp": "2025-06-15T12:03:00.000Z" }, { "id": "evt_002", "event": "DEPOSIT_CONFIRMED", "data": {}, "timestamp": "2025-06-15T12:05:00.000Z" }, { "id": "evt_003", "event": "LOCKED", "data": {}, "timestamp": "2025-06-15T12:15:00.000Z" }, { "id": "evt_004", "event": "PAYMENT_VERIFIED", "data": {}, "timestamp": "2025-06-15T12:30:00.000Z" }, { "id": "evt_005", "event": "COMPLETED", "data": {}, "timestamp": "2025-06-15T12:35:00.000Z" } ] } ``` ### Key response fields | Field | Type | Description | |-------|------|-------------| | `spread.percent` | number | Seller's profit margin as percentage | | `spread.mxn` | number | Spread in MXN | | `escrowTxHash` | string | Starknet deposit transaction hash | | `escrowStatus` | string | `"PENDING"` or `"CONFIRMED"` | | `releaseTxHash` | string | Release transaction hash (when completed) | | `buyerWallet` | string | Buyer's Starknet wallet (when locked) | | `bankAccount` | object | Seller's CLABE details | | `cepTrackingKey` | string | SPEI payment tracking key | | `amlRiskScore` | number | AML risk score (0–100) | | `amlRiskLevel` | string | `"LOW"`, `"MEDIUM"`, or `"HIGH"` | | `timeline` | array | Ordered list of order events | ### Spread calculation Spread is calculated on-demand from immutable order data, not stored: ``` spreadPercent = (pricePerUnit - fxRateUsdMxn) / fxRateUsdMxn × 100 spreadMxn = (pricePerUnit - fxRateUsdMxn) × amount ``` ### Errors | Code | HTTP | When | |------|------|------| | `NOT_FOUND` | 404 | Order doesn't exist or doesn't belong to your partner | ============================================================ ## "POST /orders & GET /orders" Source: api-reference/orders.mdx ============================================================ ## POST /orders Create an escrow order from a valid quote. ### Request ```bash curl -X POST \ -H "X-API-Key: mp2p_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "quoteId": "qt_abc123", "partnerUserRef": "user_12345", "sellerClabe": "012345678901234567" }' \ https://mexicop2p.org/api/v1/orders ``` ### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `quoteId` | string | Yes | Valid, unexpired quote ID | | `partnerUserRef` | string | Yes | Must match the quote's user ref | | `sellerClabe` | string | Yes | 18-digit CLABE (standardized Mexican bank account number) where the buyer sends pesos via SPEI | ### Response `201` ```json { "id": "ord_xyz789", "escrowOrderId": "0x1a2b3c4d5e6f...", "status": "PENDING_DEPOSIT", "token": "USDC", "amountUsdc": 100, "totalMxn": 1725.00, "exchangeRate": 17.25, "sellerClabe": "012345678901234567", "amlRiskLevel": "LOW", "expiresAt": "2025-06-15T13:03:00.000Z", "createdAt": "2025-06-15T12:03:00.000Z" } ``` ### Errors | Code | HTTP | When | |------|------|------| | `VALIDATION_ERROR` | 400 | Missing required fields | | `INVALID_CLABE` | 400 | CLABE failed validation | | `INVALID_QUOTE` | 404 | Quote not found or not yours | | `QUOTE_EXPIRED` | 410 | Quote TTL elapsed | | `QUOTE_USED` | 409 | Quote already used for another order | | `QUOTE_USER_MISMATCH` | 400 | Quote's user ref doesn't match | | `NOT_FOUND` | 404 | Partner user not found | | `VOLUME_LIMIT_EXCEEDED` | 403 | Tier volume limit exceeded | --- ## GET /orders List orders for your partner account with optional filters. ### Request ```bash curl -H "X-API-Key: mp2p_test_xxx" \ "https://mexicop2p.org/api/v1/orders?status=ACTIVE&limit=10&offset=0" ``` ### Query parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `status` | string | — | Filter by order status | | `partnerUserRef` | string | — | Filter by user reference | | `limit` | number | 20 | Max results (1–100) | | `offset` | number | 0 | Pagination offset | ### Response `200` ```json { "orders": [ { "id": "ord_xyz789", "escrowOrderId": "0x1a2b3c...", "status": "ACTIVE", "token": "USDC", "amount": 100, "totalMxn": 1725.00, "pricePerUnit": 17.25, "fxRateUsdMxn": 17.20, "amlRiskLevel": "LOW", "partnerUserRef": "user_12345", "createdAt": "2025-06-15T12:03:00.000Z", "completedAt": null, "expiresAt": "2025-06-15T13:03:00.000Z" } ], "total": 1, "limit": 10, "offset": 0, "hasMore": false } ``` ============================================================ ## "POST /quotes" Source: api-reference/quotes.mdx ============================================================ ## POST /quotes Lock the current exchange rate for 5 minutes. Use the returned quote ID to create an order. ### Request ```bash curl -X POST \ -H "X-API-Key: mp2p_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "type": "SELL", "amountUsdc": 100, "partnerUserRef": "user_12345", "feePercent": 1.5, "feeRecipient": "PARTNER" }' \ https://mexicop2p.org/api/v1/quotes ``` ### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `type` | string | Yes | Must be `"SELL"` | | `amountUsdc` | number | Yes | Amount of USDC to sell (must be > 0) | | `partnerUserRef` | string | Yes | Your internal user reference (must exist) | | `feePercent` | number | No | Your fee as a percentage of the trade (0–10%), default 0 | | `feeRecipient` | string | No | Who the fee is charged to: `"USER"`, `"PARTNER"`, or `"NONE"` (default) | MexicoP2P does not charge platform fees. The `feePercent` is your markup as a partner — you keep it (minus MexicoP2P's revenue share based on your tier). See [Pricing](/how-it-works#pricing) for details. ### Response `201` ```json { "id": "qt_abc123", "type": "SELL", "amountUsdc": 100, "exchangeRate": 17.25, "grossAmountMxn": 1725.00, "feePercent": 1.5, "feeAmountMxn": 25.88, "feeRecipient": "PARTNER", "netAmountMxn": 1699.12, "expiresAt": "2025-06-15T12:08:00.000Z" } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Quote ID — pass to `POST /orders` | | `type` | string | `"SELL"` | | `amountUsdc` | number | USDC amount | | `exchangeRate` | number | Locked USD/MXN rate from Banxico | | `grossAmountMxn` | number | Total MXN before your fee | | `feePercent` | number | Your fee percentage | | `feeAmountMxn` | number | Your fee in MXN | | `feeRecipient` | string | Who pays: `"USER"`, `"PARTNER"`, or `"NONE"` | | `netAmountMxn` | number | MXN amount after your fee | | `expiresAt` | string | Quote expires after 5 minutes | ### Errors | Code | HTTP | When | |------|------|------| | `VALIDATION_ERROR` | 400 | Missing fields or type is not `"SELL"` | | `NOT_FOUND` | 404 | `partnerUserRef` doesn't exist | ============================================================ ## "GET /users/:ref" Source: api-reference/user-detail.mdx ============================================================ ## GET /users/:ref Retrieve full details for a user by their `partnerUserRef`. ### Request ```bash curl -H "X-API-Key: mp2p_test_xxx" \ https://mexicop2p.org/api/v1/users/user_12345 ``` ### Response `200` ```json { "id": "cm3abc123", "partnerUserRef": "user_12345", "kycStatus": "APPROVED", "kycTier": "BASIC", "kycCompletedAt": "2025-06-14T10:00:00.000Z", "orderCount": 5, "totalVolumeMxn": 8625.00, "monthlyVolumeMxn": 3450.00, "amlRiskLevel": "LOW", "isPep": false, "isSanctioned": false, "createdAt": "2025-06-01T09:00:00.000Z", "updatedAt": "2025-06-15T12:00:00.000Z" } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Internal MexicoP2P user ID | | `partnerUserRef` | string | Your reference ID | | `kycStatus` | string | `NOT_REQUIRED`, `PENDING`, `APPROVED`, `REJECTED` | | `kycTier` | string | `NONE`, `BASIC`, `FULL` | | `kycCompletedAt` | string\|null | When KYC was completed | | `orderCount` | number | Total orders | | `totalVolumeMxn` | number | Lifetime volume in MXN | | `monthlyVolumeMxn` | number | Current month's volume | | `amlRiskLevel` | string\|null | `LOW`, `MEDIUM`, `HIGH`, or null | | `isPep` | boolean | Politically exposed person flag | | `isSanctioned` | boolean | Sanctions list match | | `createdAt` | string | Registration timestamp | | `updatedAt` | string | Last update timestamp | ### Errors | Code | HTTP | When | |------|------|------| | `NOT_FOUND` | 404 | User doesn't exist for this partner | ============================================================ ## "POST /users/:ref/kyc" Source: api-reference/user-kyc.mdx ============================================================ ## POST /users/:ref/kyc Create a KYC verification session. The user completes verification at the returned URL, then is redirected back to your application. ### Request ```bash curl -X POST \ -H "X-API-Key: mp2p_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "level": "BASIC", "returnUrl": "https://your-app.com/kyc/complete" }' \ https://mexicop2p.org/api/v1/users/user_12345/kyc ``` ### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `level` | string | Yes | `"BASIC"` or `"FULL"` | | `returnUrl` | string | Yes | URL to redirect user after KYC | ### Response `201` ```json { "sessionId": "did_session_abc123", "sessionUrl": "https://verify.didit.me/session/did_session_abc123" } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `sessionId` | string | Didit verification session ID | | `sessionUrl` | string | URL to redirect the user for KYC | ### KYC pricing | Level | Price per verification | |-------|----------------------| | BASIC | $1.00 | | FULL | $2.50 | Free and Growth tiers include a monthly KYC allocation. Overages are billed to the partner. ### Errors | Code | HTTP | When | |------|------|------| | `KYC_NOT_AVAILABLE` | 400 | Partner uses `PARTNER_MANAGED` KYC | | `VALIDATION_ERROR` | 400 | Missing `level` or `returnUrl` | | `NOT_FOUND` | 404 | User doesn't exist | | `KYC_LIMIT_REACHED` | 403 | Monthly KYC limit exceeded for tier | | `KYC_SESSION_FAILED` | 502 | Didit API returned an error | ============================================================ ## "POST /users" Source: api-reference/users.mdx ============================================================ ## POST /users Register a user tied to your internal user reference. Required before creating quotes or orders for that user. ### Request ```bash curl -X POST \ -H "X-API-Key: mp2p_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "partnerUserRef": "user_12345", "email": "user@example.com" }' \ https://mexicop2p.org/api/v1/users ``` ### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `partnerUserRef` | string | Yes | Your internal user ID (max 255 chars) | | `email` | string | No | User's email address | | `kycAttestation` | object | No | Only for `PARTNER_MANAGED` KYC model | | `kycAttestation.level` | string | — | `"BASIC"` or `"FULL"` | | `kycAttestation.verifiedAt` | string | — | ISO 8601 timestamp | ### Response `201` ```json { "id": "cm3abc123", "partnerUserRef": "user_12345", "kycStatus": "NOT_REQUIRED", "kycTier": "NONE", "orderCount": 0, "totalVolumeMxn": 0, "createdAt": "2025-06-15T12:01:00.000Z" } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Internal MexicoP2P user ID | | `partnerUserRef` | string | Your reference ID | | `kycStatus` | string | `NOT_REQUIRED`, `PENDING`, `APPROVED`, `REJECTED` | | `kycTier` | string | `NONE`, `BASIC`, `FULL` | | `orderCount` | number | Total orders placed | | `totalVolumeMxn` | number | Lifetime trade volume in MXN | | `createdAt` | string | Registration timestamp | ### Errors | Code | HTTP | When | |------|------|------| | `VALIDATION_ERROR` | 400 | Missing `partnerUserRef` or invalid format | | `DUPLICATE_USER` | 409 | User with this ref already exists | | `USER_LIMIT_REACHED` | 403 | Partner's tier user limit reached | ============================================================ ## "GET /webhooks" Source: api-reference/webhooks.mdx ============================================================ ## GET /webhooks Retrieve the history of webhook deliveries for your partner account. Use this to debug delivery issues or verify event receipt. ### Request ```bash curl -H "X-API-Key: mp2p_test_xxx" \ "https://mexicop2p.org/api/v1/webhooks?status=FAILED&limit=10" ``` ### Query parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `status` | string | — | Filter: `PENDING`, `DELIVERED`, `FAILED` | | `orderId` | string | — | Filter by order ID | | `eventType` | string | — | Filter by event type (e.g. `order.completed`) | | `limit` | number | 20 | Max results (1–100) | | `offset` | number | 0 | Pagination offset | ### Response `200` ```json { "deliveries": [ { "id": "del_abc123", "orderId": "ord_xyz789", "eventType": "order.completed", "status": "DELIVERED", "attempts": 1, "maxAttempts": 5, "lastAttemptAt": "2025-06-15T12:35:01.000Z", "nextAttemptAt": null, "deliveredAt": "2025-06-15T12:35:01.000Z", "errorMessage": null, "createdAt": "2025-06-15T12:35:00.000Z" }, { "id": "del_def456", "orderId": "ord_xyz789", "eventType": "order.locked", "status": "FAILED", "attempts": 5, "maxAttempts": 5, "lastAttemptAt": "2025-06-15T14:35:00.000Z", "nextAttemptAt": null, "deliveredAt": null, "errorMessage": "Connection timeout after 10s", "createdAt": "2025-06-15T12:15:00.000Z" } ], "total": 2, "limit": 10, "offset": 0, "hasMore": false } ``` ### Delivery fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique delivery ID | | `orderId` | string | Related order | | `eventType` | string | Webhook event type | | `status` | string | `PENDING`, `DELIVERED`, or `FAILED` | | `attempts` | number | Number of delivery attempts made | | `maxAttempts` | number | Maximum attempts (5) | | `lastAttemptAt` | string\|null | Last attempt timestamp | | `nextAttemptAt` | string\|null | Next retry timestamp (if pending) | | `deliveredAt` | string\|null | Successful delivery timestamp | | `errorMessage` | string\|null | Error details (if failed) | | `createdAt` | string | When the delivery was queued | ============================================================ ## How It Works Source: how-it-works.mdx ============================================================ ## The two worlds The MexicoP2P Partner API sits between **your server** and a **blockchain marketplace**. Understanding the boundary is key: | Your API calls (REST) | Marketplace (automatic) | |------------------------|------------------------| | Register users | Escrow deposit on Starknet | | Create quotes & orders | Buyer discovery & locking | | Track order status | SPEI payment & CEP verification | | Receive webhooks | Crypto release on-chain | **You create orders. The marketplace settles them.** After you call `POST /orders`, everything else happens without further API calls from you. ## Glossary | Term | Definition | |------|-----------| | **CLABE** | 18-digit standardized bank account number used in Mexico for SPEI transfers (like an IBAN) | | **SPEI** | Mexico's real-time interbank transfer system operated by Banxico (the central bank) | | **CEP** | Comprobante Electrónico de Pago — a digitally signed XML receipt issued by Banxico that proves a SPEI transfer happened | | **Escrow** | A smart contract on Starknet that holds the seller's crypto until the trade completes or is refunded | | **Starknet** | An Ethereum Layer 2 blockchain where the escrow contracts live. Partners never interact with it directly | | **Spread** | The difference between the exchange rate at order creation and the seller's asking price, expressed as a percentage | ## Your 5 API calls The entire partner integration uses just 5 endpoints: 1. **`GET /health`** — Verify your API key and check your tier 2. **`POST /users`** — Register a user (once per user) 3. **`GET /exchange-rate`** — Get the current USD/MXN rate 4. **`POST /quotes`** — Lock a rate for 5 minutes 5. **`POST /orders`** — Create an escrow order from a quote That's it. Everything after step 5 is handled by the marketplace. ## What happens after you create an order ``` YOUR SERVER MEXICOP2P STARKNET BUYER │ │ │ │ │ POST /orders │ │ │ │ ─────────────────────────► │ │ │ │ ◄── { status: PENDING } │ │ │ │ │ │ │ │ │ Seller deposits crypto │ │ │ │ ────────────────────────►│ │ │ ◄── webhook: ACTIVE │ │ │ │ │ │ │ │ │ Buyer locks order │ │ │ ◄───────────────────────────────────│ │ ◄── webhook: LOCKED │ │ │ │ │ │ │ │ │ │ Buyer sends MXN │ │ │ │ via SPEI │ │ │ │ │ │ │ CEP verified │ │ │ │ ────────────────────────►│ Release crypto │ │ ◄── webhook: COMPLETED │ │ ────────────────────►│ │ │ │ │ ``` **Key takeaway:** After `POST /orders`, you just listen for webhooks. You don't need to call any more endpoints to complete the trade. ## Order statuses | Status | What happened | What you should do | |--------|--------------|-------------------| | `PENDING_DEPOSIT` | Order created, waiting for seller to deposit crypto into escrow | Wait for `order.deposit_confirmed` webhook | | `ACTIVE` | Crypto is in escrow, order visible to buyers on marketplace | Wait for `order.locked` webhook | | `LOCKED` | A buyer committed to purchase, has 24h to send MXN via SPEI | Wait for `order.payment_verified` or `order.lock_expired` webhook | | `PENDING_RELEASE` | SPEI payment verified via CEP, crypto being released on-chain | Wait for `order.completed` webhook | | `COMPLETED` | Trade finished — buyer got crypto, seller got MXN | Update your UI. Terminal state | | `EXPIRED` | No deposit or no buyer before timeout | Clean up. Terminal state | | `REFUNDED` | Crypto returned to seller (lock expired, cancelled, or dispute) | Clean up. Terminal state | | `DISPUTED` | Either party opened a dispute on the escrow contract | Wait for `order.dispute_resolved` webhook | ### Status flow diagram ``` Happy path ---------- PENDING_DEPOSIT ---> ACTIVE ---> LOCKED ---> PENDING_RELEASE ---> COMPLETED Unhappy paths ------------- PENDING_DEPOSIT ---> EXPIRED (no deposit before timeout) ACTIVE -----------> EXPIRED (no buyer before timeout) LOCKED -----------> REFUNDED (lock expired, no payment) LOCKED -----------> DISPUTED ------> COMPLETED (resolved for buyer) |--> REFUNDED (resolved for seller) |--> LOCKED (no-fault, retry with new 24h timer) ``` ## KYC: Two models MexicoP2P supports two approaches to Know Your Customer verification. Your model is chosen when your partner account is created — it applies to all your users. ### Option A: Partner-managed You already verify identities on your side (e.g., you're a regulated fintech). When registering users, you attest to their KYC level: ```bash curl -X POST \ -H "X-API-Key: mp2p_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "partnerUserRef": "user_12345", "kycAttestation": { "level": "BASIC", "verifiedAt": "2025-06-01T00:00:00Z" } }' \ https://mexicop2p.org/api/v1/users ``` You're responsible for verifying the user meets the requirements for the level you attest to. With this model, `POST /users/:ref/kyc` returns `KYC_NOT_AVAILABLE` since verification happens on your side. ### Option B: MexicoP2P-provided We handle identity verification through our provider (Didit). You redirect users to a verification URL when they need to level up: ```bash # 1. Register the user (no attestation needed) curl -X POST \ -H "X-API-Key: mp2p_test_xxx" \ -H "Content-Type: application/json" \ -d '{"partnerUserRef": "user_12345"}' \ https://mexicop2p.org/api/v1/users # 2. Start a KYC session when the user needs to verify curl -X POST \ -H "X-API-Key: mp2p_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "level": "BASIC", "returnUrl": "https://your-app.com/kyc/complete" }' \ https://mexicop2p.org/api/v1/users/user_12345/kyc ``` The response contains a `sessionUrl` — redirect the user there. When they finish, they're sent back to your `returnUrl`. Each verification session costs $1.00 (BASIC) or $2.50 (FULL), billed to the partner. Free and Growth tiers include a monthly allocation before overages apply. ### BASIC vs FULL verification | | BASIC | FULL | |---|---|---| | **What's checked** | Government-issued ID (INE/passport) + selfie liveness check | Everything in BASIC + proof of address (utility bill, bank statement) + enhanced screening | | **Volume limit** | Up to 50,000 MXN/month | Unlimited | | **Typical use** | Most users | High-volume traders | ### When is KYC required? KYC is **not required to start trading**. New users can trade immediately. Verification is only triggered when a user's cumulative volume hits a threshold: | Cumulative volume | Required KYC | What happens | |---|---|---| | Under 10,000 MXN/month | None | User trades freely | | 10,000 – 50,000 MXN/month | BASIC | Next `POST /orders` returns `KYC_REQUIRED` until BASIC is completed | | Over 50,000 MXN/month | FULL | Next `POST /orders` returns `KYC_REQUIRED` until FULL is completed | When a user hits a threshold, their next order creation fails with `KYC_REQUIRED`. You then either submit a `kycAttestation` (partner-managed) or start a KYC session (MexicoP2P-provided) to unlock further trading. ## Dispute resolution Disputes can happen during `LOCKED` status — for example, the buyer claims they sent payment but the seller says they didn't receive it, or the CEP can't be verified due to a Banxico outage. ### How disputes work 1. **Either party opens a dispute** on the Starknet escrow contract. The order moves to `DISPUTED` status. You receive an `order.disputed` webhook. 2. **A MexicoP2P admin reviews the evidence** — CEP records, transaction history, user reputation, and fraud scoring. 3. **The admin resolves the dispute.** You receive an `order.dispute_resolved` webhook with the outcome. ### Three possible outcomes | Outcome | What happens to the crypto | What happens to reputation | When this applies | |---|---|---|---| | **Resolved for buyer** | Released to buyer | Seller loses 50 points | Buyer paid but seller denies receiving it; CEP proves payment | | **Resolved for seller** | Refunded to seller | Buyer loses 50 points | Buyer never paid or submitted a fraudulent CEP | | **No-fault** | Depends on sub-outcome (see below) | No penalty for either party | External issue — neither party is at fault | ### No-fault sub-outcomes When a dispute is nobody's fault (e.g., Banxico was down, a network error prevented the CEP from being verified), the admin chooses one of these: - **Release to buyer** — Buyer actually paid (has a valid receipt outside the system). Crypto released to buyer. Order → `COMPLETED`. - **Refund to seller** — Buyer couldn't complete payment due to the external issue. Crypto returned to seller. Order → `REFUNDED`. - **Retry** — Temporary issue is resolved. Order returns to `LOCKED` with a fresh 24-hour timer so the buyer can try again. ### What you need to do Nothing. Disputes are handled entirely by the marketplace. Just listen for the `order.dispute_resolved` webhook and update your UI based on the final order status (`COMPLETED`, `REFUNDED`, or back to `LOCKED`). ## Pricing **MexicoP2P does not charge fees to end users.** There is no platform fee on trades. ### Your fee, your choice As a partner, you decide whether to charge your users a fee on each trade. When creating a quote, you can pass `feePercent` (0–10%) and `feeRecipient` to add a markup: ``` Exchange rate (from Banxico): 17.25 MXN/USD 100 USDC at that rate: 1,725.00 MXN Your fee (e.g., 1.5%): − 25.88 MXN Net to user: 1,699.12 MXN ``` If you don't pass `feePercent`, it defaults to 0 — the user gets the raw exchange rate with no markup. ### Revenue share MexicoP2P takes a percentage of the fees you collect, based on your tier: | Tier | MexicoP2P's share of your fees | |------|-------------------------------| | **Free** | 30% | | **Growth** | 20% | | **Enterprise** | Custom (negotiated) | If you charge no fees (`feePercent: 0`), there's nothing to share — you pay nothing. ### Where to see your revenue - **Partner Dashboard** — Your total fees collected and MexicoP2P's revenue share are shown in the **Revenue** tab. - **Admin Dashboard** — Platform-wide revenue across all partners is visible under **Partners > Revenue**. ### Quote response fields | Field | Description | |-------|-------------| | `exchangeRate` | Base USD/MXN rate from Banxico | | `grossAmountMxn` | Total MXN before your fee | | `feePercent` | Your fee percentage (0 if not set) | | `feeAmountMxn` | Your fee in MXN | | `feeRecipient` | `"PARTNER"`, `"USER"`, or `"NONE"` | | `netAmountMxn` | MXN amount after your fee | ============================================================ ## Getting Started Source: index.mdx ============================================================ ## What is the MexicoP2P Partner API? MexicoP2P provides **crypto-to-peso infrastructure** for your platform. You make a few REST API calls to create USDC/USDT sell orders, and the MexicoP2P marketplace handles escrow on Starknet, buyer matching, SPEI payment verification, and crypto release — all automatically. **Base URL:** ``` https://mexicop2p.org/api/v1 ``` ## Get your API key 1. Sign up for a partner account at [mexicop2p.org/partners](https://mexicop2p.org/partners) 2. Open the **Partner Dashboard** and navigate to **API Keys** 3. Click **Generate API Key** — copy it immediately (we hash it with bcrypt and can't show it again) 4. Copy your **Webhook Secret** from the same page (used to verify webhook signatures) ### Key format | Part | Value | Length | |------|-------|--------| | Prefix | `mp2p_` | 5 chars | | Secret | Random hex | 64 chars | | **Total** | | **69 chars** | Example: `mp2p_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2` ### Key management - Each partner gets one active key at a time - Rotate keys by generating a new one and updating your integration - Keys cannot be recovered — generate a new one if lost ## Authentication All requests require your API key in the `X-API-Key` header: ```bash curl -H "X-API-Key: mp2p_your_api_key_here" \ https://mexicop2p.org/api/v1/health ``` ### Authentication errors | Error Code | HTTP | Meaning | |-----------|------|---------| | `MISSING_API_KEY` | 401 | No `X-API-Key` header in the request | | `INVALID_API_KEY` | 401 | Key doesn't match any active partner | | `PARTNER_INACTIVE` | 403 | Partner account is not active | | `PARTNER_SUSPENDED` | 403 | Partner KYB status is suspended | ## IP whitelisting You can restrict API access to specific IP addresses from the Partner Dashboard. When enabled, requests from non-whitelisted IPs receive a `403` with error code `IP_NOT_ALLOWED`. Leave the whitelist empty to allow all IPs. ## Webhook signatures Webhook deliveries include HMAC-SHA256 signatures so you can verify they came from MexicoP2P. See [Signature Verification](/webhooks/signature-verification) for implementation details. | Header | Description | |--------|-------------| | `X-Webhook-Id` | Unique delivery ID | | `X-Webhook-Signature` | HMAC-SHA256 hex signature | | `X-Webhook-Timestamp` | Unix timestamp (seconds) | Signature is computed over `{timestamp}.{JSON payload}` using your webhook secret. ## Rate limits and tiers | Tier | Requests/min | Monthly users | Monthly volume | Revenue share | Price | |------|-------------|---------------|----------------|---------------|-------| | **Free** | 50 | 10 | 50,000 MXN | 30% of your fees | $0 | | **Growth** | 200 | 500 | 1,000,000 MXN | 20% of your fees | $199/mo | | **Enterprise** | 1,000 | Unlimited | Custom | Custom | Custom | MexicoP2P does not charge fees to end users. You optionally set a `feePercent` on quotes — that's your revenue. MexicoP2P takes a share of it based on your tier. See [Pricing](/how-it-works#pricing). ### Rate limit headers Every response includes these headers: | Header | Description | |--------|-------------| | `X-RateLimit-Limit` | Max requests per minute for your tier | | `X-RateLimit-Remaining` | Requests remaining in the current window | | `X-RateLimit-Reset` | Unix timestamp when the window resets | | `Retry-After` | Seconds to wait (only on `429` responses) | When you exceed the limit, you'll receive a `429` response with error code `RATE_LIMITED`. Use the `Retry-After` header value before retrying. ### Volume and user limits - **Volume limits** are checked on each `POST /orders` request (daily and monthly). Returns `VOLUME_LIMIT_EXCEEDED` when exceeded. - **User limits** are checked on `POST /users`. Returns `USER_LIMIT_REACHED` when exceeded. ### Best practices - Cache exchange rates (valid for 5 minutes) instead of fetching per request - Use webhooks instead of polling order status - Implement exponential backoff when receiving `429` responses ## Error codes All errors follow a consistent format: ```json { "error": "ERROR_CODE", "message": "Human-readable error description" } ``` ### Full error code table | Code | HTTP | Description | |------|------|-------------| | `MISSING_API_KEY` | 401 | No `X-API-Key` header in request | | `INVALID_API_KEY` | 401 | API key doesn't match any active partner | | `PARTNER_INACTIVE` | 403 | Partner account is not active | | `PARTNER_SUSPENDED` | 403 | Partner KYB status is suspended | | `IP_NOT_ALLOWED` | 403 | Client IP not in partner's whitelist | | `RATE_LIMITED` | 429 | Request rate limit exceeded for tier | | `VALIDATION_ERROR` | 400 | Missing or invalid request fields | | `INVALID_CLABE` | 400 | CLABE failed 18-digit validation with check digit | | `INVALID_QUOTE` | 404 | Quote not found or doesn't belong to partner | | `QUOTE_EXPIRED` | 410 | Quote TTL (5 minutes) has elapsed | | `QUOTE_USED` | 409 | Quote already consumed by an order | | `QUOTE_USER_MISMATCH` | 400 | Quote's `partnerUserRef` doesn't match request | | `NOT_FOUND` | 404 | Resource doesn't exist | | `DUPLICATE_USER` | 409 | User with this `partnerUserRef` already exists | | `USER_LIMIT_REACHED` | 403 | Partner hit tier's maximum user count | | `VOLUME_LIMIT_EXCEEDED` | 403 | Partner exceeded daily or monthly volume limit | | `KYC_NOT_AVAILABLE` | 400 | Partner uses `PARTNER_MANAGED` KYC model | | `KYC_LIMIT_REACHED` | 403 | Monthly KYC verification limit exceeded | | `KYC_SESSION_FAILED` | 502 | Upstream KYC provider returned an error | ### HTTP status code summary | Status | Meaning | |--------|---------| | 200 | Success | | 201 | Created (new resource) | | 400 | Validation error | | 401 | Authentication failed | | 403 | Forbidden (inactive, suspended, limit reached, IP blocked) | | 404 | Not found | | 409 | Conflict (duplicate, already used) | | 410 | Gone (expired) | | 429 | Rate limited | | 500 | Internal server error | | 502 | Bad gateway (external service error) | ## Quick links - [How It Works](/how-it-works) — Architecture, trade flow, glossary, and KYC models - [Quickstart](/quickstart) — Create your first order in 5 steps - [API Reference](/api-reference/health) — All endpoints with examples - [Webhooks](/webhooks/overview) — Event delivery and signature verification ## LLM context AI coding assistants can use machine-readable API context files to help you integrate: | File | URL | Description | |------|-----|-------------| | `llms.txt` | [/llms.txt](https://docs.mexicop2p.org/llms.txt) | Concise API overview for AI context windows | | `llms-full.txt` | [/llms-full.txt](https://docs.mexicop2p.org/llms-full.txt) | Full documentation as plain text | Add to your project's AI config (e.g., `.cursor/rules` or `CLAUDE.md`): ``` @https://docs.mexicop2p.org/llms.txt ``` ============================================================ ## Quickstart Source: quickstart.mdx ============================================================ ## Before you start You'll need: - A MexicoP2P partner **API key** (format: `mp2p_xxx`) — see [Getting Started](/#get-your-api-key) - A **webhook URL** on your server (HTTPS) to receive order status updates Replace `mp2p_test_xxx` with your actual API key in all examples below. ## Step 1: Verify your API key ```bash curl -H "X-API-Key: mp2p_test_xxx" \ https://mexicop2p.org/api/v1/health ``` ```json { "status": "ok", "partner": "Acme Finance", "tier": "GROWTH", "timestamp": "2025-06-15T12:00:00.000Z" } ``` If you get a `401`, double-check your key. See [error codes](/#error-codes) for details. ## Step 2: Register a user Create a user record tied to your internal user ID. You only need to do this once per user. ```bash curl -X POST \ -H "X-API-Key: mp2p_test_xxx" \ -H "Content-Type: application/json" \ -d '{"partnerUserRef": "user_12345"}' \ https://mexicop2p.org/api/v1/users ``` ```json { "id": "cm3abc123", "partnerUserRef": "user_12345", "kycStatus": "NOT_REQUIRED", "kycTier": "NONE", "orderCount": 0, "totalVolumeMxn": 0, "createdAt": "2025-06-15T12:01:00.000Z" } ``` `partnerUserRef` is your internal user ID — use whatever identifier makes sense in your system. ## Step 3: Get a rate and create a quote First, check the current exchange rate: ```bash curl -H "X-API-Key: mp2p_test_xxx" \ https://mexicop2p.org/api/v1/exchange-rate ``` ```json { "usdMxn": 17.25, "source": "banxico", "timestamp": "2025-06-15T12:02:00.000Z", "validFor": 300 } ``` Then lock that rate by creating a quote (valid for 5 minutes). You can optionally set a `feePercent` — your markup on the trade: ```bash curl -X POST \ -H "X-API-Key: mp2p_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "type": "SELL", "amountUsdc": 100, "partnerUserRef": "user_12345", "feePercent": 1.5, "feeRecipient": "PARTNER" }' \ https://mexicop2p.org/api/v1/quotes ``` ```json { "id": "qt_abc123", "type": "SELL", "amountUsdc": 100, "exchangeRate": 17.25, "grossAmountMxn": 1725.00, "feePercent": 1.5, "feeAmountMxn": 25.88, "feeRecipient": "PARTNER", "netAmountMxn": 1699.12, "expiresAt": "2025-06-15T12:08:00.000Z" } ``` MexicoP2P doesn't charge fees to users — `feePercent` is your optional markup as a partner. If you don't set it, it defaults to 0 and the user gets the raw exchange rate. See [Pricing](/how-it-works#pricing) for details. ## Step 4: Create an order Convert the quote into a live escrow order: ```bash curl -X POST \ -H "X-API-Key: mp2p_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "quoteId": "qt_abc123", "partnerUserRef": "user_12345", "sellerClabe": "012345678901234567" }' \ https://mexicop2p.org/api/v1/orders ``` ```json { "id": "ord_xyz789", "escrowOrderId": "0x1a2b3c...", "status": "PENDING_DEPOSIT", "token": "USDC", "amountUsdc": 100, "totalMxn": 1725.00, "exchangeRate": 17.25, "sellerClabe": "012345678901234567", "amlRiskLevel": "LOW", "expiresAt": "2025-06-15T13:03:00.000Z", "createdAt": "2025-06-15T12:03:00.000Z" } ``` `sellerClabe` is the seller's 18-digit CLABE (Mexican standardized bank account number) where the buyer will send pesos via SPEI. ## Step 5: Track the order ### Option A: Webhooks (recommended) Configure your webhook URL in the Partner Dashboard. You'll receive events like: ```json { "event": "order.completed", "data": { "orderId": "ord_xyz789", "status": "COMPLETED", "completedAt": "2025-06-15T14:30:00.000Z" }, "timestamp": "2025-06-15T14:30:01.000Z" } ``` See [Webhook Overview](/webhooks/overview) for all event types and [Signature Verification](/webhooks/signature-verification) for security. ### Option B: Polling If webhooks aren't set up yet, you can poll the order status: ```bash curl -H "X-API-Key: mp2p_test_xxx" \ https://mexicop2p.org/api/v1/orders/ord_xyz789 ``` Poll at most once per minute. Prefer webhooks for production. ## Complete Node.js example ```javascript const API_KEY = "mp2p_test_xxx"; const BASE = "https://mexicop2p.org/api/v1"; const headers = { "X-API-Key": API_KEY, "Content-Type": "application/json", }; // 1. Register a user (once) const user = await fetch(`${BASE}/users`, { method: "POST", headers, body: JSON.stringify({ partnerUserRef: "user_12345" }), }).then((r) => r.json()); // 2. Create a quote (feePercent is optional — your markup) const quote = await fetch(`${BASE}/quotes`, { method: "POST", headers, body: JSON.stringify({ type: "SELL", amountUsdc: 100, partnerUserRef: "user_12345", feePercent: 1.5, feeRecipient: "PARTNER", }), }).then((r) => r.json()); // 3. Create an order const order = await fetch(`${BASE}/orders`, { method: "POST", headers, body: JSON.stringify({ quoteId: quote.id, partnerUserRef: "user_12345", sellerClabe: "012345678901234567", }), }).then((r) => r.json()); console.log(`Order ${order.id} created — status: ${order.status}`); // Now wait for webhooks to track progress ``` ## What's next - [How It Works](/how-it-works) — Understand the full trade flow, KYC models, and dispute resolution - [API Reference](/api-reference/health) — Complete endpoint documentation - [Webhooks](/webhooks/overview) — Set up real-time event notifications ============================================================ ## Webhook Overview Source: webhooks/overview.mdx ============================================================ ## Events MexicoP2P sends webhook notifications for order lifecycle events. Configure your webhook URL in the Partner Dashboard. | Event | When | |-------|------| | `order.created` | Order placed, entering `PENDING_DEPOSIT` | | `order.deposit_confirmed` | Escrow deposit confirmed on Starknet | | `order.locked` | Buyer locked the order | | `order.payment_verified` | SPEI payment (CEP) validated | | `order.completed` | Escrow released, trade finished | | `order.refunded` | Crypto refunded to seller | | `order.lock_expired` | Buyer didn't complete payment in time | | `order.disputed` | Dispute opened by either party | | `order.dispute_resolved` | Dispute resolved by admin | ## Payload format All webhook payloads follow the same structure: ```json { "event": "order.completed", "timestamp": "2025-06-15T12:35:00.000Z", "data": { "orderId": "ord_xyz789", "escrowOrderId": "0x1a2b3c...", "status": "COMPLETED", "partnerUserRef": "user_12345", "token": "USDC", "amount": 100, "totalMxn": 1725.00, "exchangeRate": 17.25, "buyerWallet": "0x04a1b2c3...", "lockedAt": "2025-06-15T12:15:00.000Z", "completedAt": "2025-06-15T12:35:00.000Z", "createdAt": "2025-06-15T12:03:00.000Z" } } ``` ### Payload fields | Field | Type | Description | |-------|------|-------------| | `event` | string | Event type | | `timestamp` | string | When the event occurred | | `data.orderId` | string | Order ID | | `data.escrowOrderId` | string | Starknet escrow order ID | | `data.status` | string | Current order status | | `data.partnerUserRef` | string | Your user reference | | `data.token` | string | `USDC` or `USDT` | | `data.amount` | number | Token amount | | `data.totalMxn` | number | Total MXN value | | `data.exchangeRate` | number | USD/MXN rate | | `data.buyerWallet` | string\|null | Buyer's Starknet wallet | | `data.lockedAt` | string\|null | When buyer locked | | `data.completedAt` | string\|null | When trade completed | | `data.createdAt` | string | Order creation time | ## Headers Each webhook delivery includes these headers: | Header | Description | |--------|-------------| | `X-Webhook-Id` | Unique delivery ID | | `X-Webhook-Signature` | HMAC-SHA256 hex signature | | `X-Webhook-Timestamp` | Unix timestamp (seconds) | | `Content-Type` | `application/json` | ## Delivery - **Timeout**: 10 seconds per attempt - **Success**: Any `2xx` HTTP response - **Failure**: Non-2xx response or timeout ## Retry policy Failed deliveries are retried with exponential backoff: | Attempt | Delay after failure | |---------|-------------------| | 1st retry | 30 seconds | | 2nd retry | 2 minutes | | 3rd retry | 10 minutes | | 4th retry | 30 minutes | | 5th retry | 2 hours | After 5 failed attempts, the delivery is marked as `FAILED`. Check failed deliveries via `GET /webhooks?status=FAILED`. ## Best practices - Return `200 OK` quickly — process the webhook payload asynchronously - Use the `X-Webhook-Id` for idempotency (you may receive the same event twice) - Verify the signature before processing (see [Signature Verification](/webhooks/signature-verification)) - Store the raw payload for debugging ============================================================ ## Signature Verification Source: webhooks/signature-verification.mdx ============================================================ ## How it works Every webhook delivery is signed with HMAC-SHA256 using your webhook secret. The signature is computed over the string `{timestamp}.{raw JSON body}`. ### Verification steps 1. Extract the `X-Webhook-Signature` and `X-Webhook-Timestamp` headers 2. Concatenate: `"{timestamp}.{raw request body}"` 3. Compute HMAC-SHA256 with your webhook secret 4. Compare the computed signature with the header value 5. Optionally check that the timestamp is within 5 minutes to prevent replay attacks ## Node.js ```javascript const crypto = require('crypto'); function verifyWebhookSignature(secret, payload, timestamp, signature) { const data = `${timestamp}.${payload}`; const expected = crypto .createHmac('sha256', secret) .update(data) .digest('hex'); // Timing-safe comparison to prevent timing attacks return crypto.timingSafeEqual( Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex') ); } // Express middleware example app.post('/webhooks/mexicop2p', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-webhook-signature']; const timestamp = req.headers['x-webhook-timestamp']; const payload = req.body.toString(); // Check timestamp is within 5 minutes const now = Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(timestamp)) > 300) { return res.status(400).send('Timestamp too old'); } if (!verifyWebhookSignature(process.env.WEBHOOK_SECRET, payload, timestamp, signature)) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(payload); console.log(`Received ${event.event} for order ${event.data.orderId}`); // Process asynchronously processWebhook(event); res.status(200).send('ok'); }); ``` ## Python ```python from flask import Flask, request, abort app = Flask(__name__) WEBHOOK_SECRET = "your_webhook_secret" def verify_webhook_signature(secret, payload, timestamp, signature): data = f"{timestamp}.{payload}" expected = hmac.new( secret.encode('utf-8'), data.encode('utf-8'), hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected) @app.route('/webhooks/mexicop2p', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-Webhook-Signature') timestamp = request.headers.get('X-Webhook-Timestamp') payload = request.get_data(as_text=True) # Check timestamp is within 5 minutes if abs(time.time() - int(timestamp)) > 300: abort(400, 'Timestamp too old') if not verify_webhook_signature(WEBHOOK_SECRET, payload, timestamp, signature): abort(401, 'Invalid signature') event = request.get_json() print(f"Received {event['event']} for order {event['data']['orderId']}") return 'ok', 200 ``` ## Testing with curl Generate a test signature and send a webhook to your local server: ```bash # Set your webhook secret SECRET="your_webhook_secret" TIMESTAMP=$(date +%s) PAYLOAD='{"event":"order.completed","timestamp":"2025-06-15T12:35:00.000Z","data":{"orderId":"ord_test","status":"COMPLETED"}}' # Compute HMAC-SHA256 SIGNATURE=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') # Send test webhook curl -X POST http://localhost:3000/webhooks/mexicop2p \ -H "Content-Type: application/json" \ -H "X-Webhook-Id: del_test_001" \ -H "X-Webhook-Signature: $SIGNATURE" \ -H "X-Webhook-Timestamp: $TIMESTAMP" \ -d "$PAYLOAD" ``` ## Security notes - Store your webhook secret securely (environment variable, secrets manager) - Always verify signatures before processing payloads - Use the raw request body for signature verification (not parsed JSON) - Implement timestamp checking to prevent replay attacks - Use timing-safe comparison functions to prevent timing attacks