# API Documentation Payment processing platform that enables stores to accept payments, process transfers, and integrate with e-commerce platforms. All API requests require Bearer token authentication via the Authorization header. ## Authentication All endpoints use the Authorization header with a Bearer token. Two distinct keys authenticate requests: - Payment, payment link and store-info routes: Authorization: Bearer - Transfer and balance routes: Authorization: Bearer (withdrawal key) Both keys are available in the dashboard under Finance → Integrations. Using the wrong key for a route returns 401 ("Invalid or expired token.", code jwt.authentication.invalidToken). A key from an inactive store returns 401 store.authentication.inactiveStore, and one from a blocked user returns 401 auth.authentication.blockedUser. A third credential, the tokenization key, is not used for authentication. It only encrypts card data in the browser before a credit card payment. It is publishable; the two keys above are secret and must never reach the front-end. See Card Tokenization below. ## Base URL Base URL: https://api.sejaboop.com All endpoints are versioned under /v1/. ## Standards and Formats - Currency: ISO 4217 (3-letter code, e.g., BRL, USD). Reference: https://en.wikipedia.org/wiki/ISO_4217 - Amounts: always sent in the currency's smallest unit (cents in BRL) as integer. Example: R$ 100.00 = 10000 - Dates: ISO 8601 in UTC (UTC+00:00). Example: 2026-01-15T16:51:42.782Z ## Enums Values accepted in payment creation and related routes: - method: PIX | CREDIT_CARD | BOLETO - currency: BRL (ISO 4217) - items[].type (productType): DIGITAL | PHYSICAL - payment.status: PENDING | PROCESSING | PAID | REFUSED | REFUNDED | MED | CHARGEDBACK - transfer.status: IN_QUEUE | IN_ANALYSIS | APPROVED | PROCESSING | COMPLETED | REFUSED | FAILED - pixKeyType: CPF | CNPJ | EMAIL | PHONE | EVP | COPYPASTE Payment methods by store currency: | Store currency | Methods | | --- | --- | | BRL | PIX, CREDIT_CARD, BOLETO | The payment currency must match the store currency. The selected method must also be active for that store. ## Store ### GET /v1/balance Returns the store's current financial balance, in the store currency. Requires the withdrawal key. A store with no wallet yet gets every value as zero. Response (200): ```json { "available": 5000, "transfers": { "pending": 180 }, "reserve": { "pending": 320 }, "receivables": { "pending": 0 }, "anticipations": { "pending": 0 }, "todayPayments": 2000, "todayTransfers": 1000 } ``` Status codes: 200, 401, 429 ### GET /v1/store-info Returns public store details of the store that owns the key. Requires the payment API key (not the withdrawal key). Owner data (name, email, tax ID, internal IDs) is not exposed here; taxId comes masked. status is always ACTIVE, since a key from an inactive store gets 401. Response (200): ```json { "id": "as9xk3l2p0q8w7e6r5t4y1", "name": "Store Example", "type": "PF", "taxId": "***.456.789-**", "status": "ACTIVE", "currency": "BRL", "createdAt": "2026-01-10T14:22:31.120Z", "updatedAt": "2026-03-28T09:11:45.532Z" } ``` Status codes: 200, 401, 429 ## Payments ### POST /v1/payment Creates a new payment. Supports PIX, CREDIT_CARD, BOLETO methods. ⚠️ Amount must always be sent in the currency's smallest unit (integer). The currency must be the store's currency. Request body (BRL/PIX example, for BRL stores): ```json { "amount": 10000, "currency": "BRL", "method": "PIX", "description": "Order payment", "externalRef": "order_123456", "notificationUrl": "https://example.com/webhook", "ip": "203.0.113.10", "payer": { "name": "John Doe", "taxId": "12345678909", "email": "john.doe@example.com", "phone": "+5511999990000" }, "items": [{ "quantity": 1, "name": "Product", "price": 10000, "type": "PHYSICAL" }], "delivery": { "fee": 1000, "address": { "country": "BR", "state": "SP", "city": "São Paulo", "district": "Centro", "street": "Rua Exemplo", "number": "123", "zipCode": "01001-000" } }, "metadata": { "provider": "my-platform", "orderId": "ord_123", "sellerTaxId": "98765432100", "sellerEmail": "seller@example.com", "checkoutUrl": null, "returnUrl": null, "shopUrl": null, "referrerLink": null, "extra": null }, "utms": { "utmSource": "google", "utmMedium": "cpc", "utmCampaign": "black-friday-2026", "utmContent": "top-banner", "utmTerm": "running-shoes", "src": "blog-post-1", "sck": "warm-list" } } ``` Request rules: - description: 5 to 255 chars. items[].name: 3 to 256 chars. ip: up to 45 chars. metadata.provider: up to 50 chars. - ip is the buyer's IP, as your backend receives it in the buyer's request, not your server's IP. Some banks use it to authenticate the card. Without a valid IP, the payment goes on without it. - payer.taxId is required only when currency is BRL, and then it must be a valid CPF or CNPJ. - payer.email may be required by some payment methods; send it whenever you have it. - delivery is required when any item is PHYSICAL. delivery.fee is not added to amount. - For method CREDIT_CARD send "card": { "token": "", "installments": 1 to 12 in BRL, only 1 in other currencies, "chargeInterest": false, "billingAddress": optional, "threeDS": optional }. With chargeInterest true and more than one installment, the installment interest is passed on to the payer and the payment amount goes up; the default false takes the fee out of what the store receives. - card.threeDS: optional, the object returned by threeDS.start() in the 3DS SDK. Send it as it came, without building or changing fields: an unknown field returns 400, except in device, which is dropped when out of format so the sale goes on without it. All its fields are optional: authentication (id, eci, cavv, dsTransactionId, version, status: result of the authentication done before the sale), device (sessionId, fingerprint, userAgent, language, screenWidth, screenHeight, colorDepth, timeZoneOffset: fraud analysis data from the buyer's browser) and cardReference (the card in the format that charging some cards requires, generated in the browser; it does not replace card.token). - "boleto": { "billingAddress": ... } is optional for method BOLETO. - metadata.returnUrl: when the card authentication opens inside your page (see 3DS SDK), it must be on the same origin as the payment page (scheme, domain and port) and be allowed in an iframe: no X-Frame-Options: DENY and no frame-ancestors 'none' (X-Frame-Options: SAMEORIGIN works). - splits: optional list of split IDs (cuid) created in the store, in the same currency as the payment, e.g. ["cm5t0d4yz0004ls08s1v7n2qf"]. An unknown ID or one in another currency returns 400 payment.validation.invalidSplits; a repeated ID returns 400 payment.validation.duplicatedSplits. Response (200), BRL/PIX example, right after creation: ```json { "id": "cm6p1x8k70001ls08h2q9d3vz", "amount": 10000, "method": "PIX", "currency": "BRL", "status": "PENDING", "description": "Order payment", "installments": 1, "discount": null, "payer": { "name": "John Doe", "taxId": "12345678909", "email": "john.doe@example.com", "phone": "+5511999990000" }, "externalRef": "order_123456", "notificationUrl": "https://example.com/webhook", "metadata": null, "utms": { "utmSource": "google", "utmMedium": "cpc", "utmCampaign": "black-friday-2026", "utmContent": "top-banner", "utmTerm": "running-shoes", "src": "blog-post-1", "sck": "warm-list" }, "redirectUrl": null, "paidAt": null, "refundedAt": null, "createdAt": "2026-01-15T13:44:25.838Z", "updatedAt": "2026-01-15T13:44:25.838Z", "orderId": null, "data": { "method": "PIX", "copypaste": "00020101..." }, "splits": [{ "amount": 10000, "currency": "BRL", "percent": 100, "storeId": "cm5t0a9qz0000ls08p4r7c2kd", "splitId": null }], "delivery": { "fee": 1000, "address": { "zipCode": "01001-000", "country": "BR", "state": "SP", "city": "São Paulo", "neighborhood": "Centro", "line1": "Rua Exemplo", "line2": null, "streetNumber": "123" } }, "items": [{ "quantity": 1, "name": "Product", "price": 10000, "type": "PHYSICAL" }] } ``` Response fields worth knowing: - orderId: order of the platform's checkout when the payment comes from there; null for payments created through this API. Your own reference comes back in externalRef. - discount: payment method discount set in the checkout, in cents, already taken out of amount. Does not include coupons. null for payments created through this API. - redirectUrl: URL the payer must be sent to in order to complete the payment, when the method requires that step; null otherwise. - threeDSToken: optional, only in this create response, when a CREDIT_CARD payment waits for the buyer's authentication after the sale and the 3DS SDK has to confirm it. Valid for 15 minutes. It is not stored: webhooks and GET /v1/payment/:id never carry it. Do not use it directly: pass the whole response to threeDS.finish(). - splits[].percent: percentage points (100 = 100%), may have decimals. - delivery.address always comes in one format, whatever the request format: zipCode, country, state, city, neighborhood, line1, line2, streetNumber (district -> neighborhood, street -> line1, complement -> line2, number -> streetNumber). - data.e2e (PIX) is filled in after the payment is confirmed. data fields by method: - PIX: copypaste, e2e - CREDIT_CARD: message, cardHolder, cardNumber, cardExpMonth, cardExpYear, cardChargeInterest, card3DS (only when the payer must authenticate in the browser) - BOLETO: barcode, digitableLine Status codes: 200, 400, 401, 403 (payment.authorization.invalidCurrency, payment.authorization.invalidMethod), 422 (payment.businessLogic.ticketMin, payment.businessLogic.ticketMax, payment.businessLogic.netAmountLT0, payment.businessLogic.installmentsNotAllowed), 429, 502 (the payment could not be processed; body has only "message") ### GET /v1/payment/:id Returns a payment of the store that owns the key. Same object as the create response and the payment webhook, except that "delivery" is always null on this route. Response (200), BRL/PIX example: ```json { "id": "cm6p2b7n40001ls08q3w5e8rt", "amount": 15990, "method": "PIX", "currency": "BRL", "status": "PAID", "description": "Compra de produto digital", "installments": 1, "discount": null, "payer": { "name": "Maria Silva Santos", "taxId": "32165498700", "email": "maria.silva@email.com", "phone": "+5511988887777" }, "externalRef": "order_7h3d92ksl1p0x", "notificationUrl": "https://example.com/webhook", "metadata": null, "utms": { "utmSource": "google", "utmMedium": "cpc", "utmCampaign": "black-friday-2026", "utmContent": "top-banner", "utmTerm": "running-shoes", "src": "blog-post-1", "sck": "warm-list" }, "redirectUrl": null, "paidAt": "2026-02-10T14:21:40.551Z", "refundedAt": null, "createdAt": "2026-02-10T14:21:33.102Z", "updatedAt": "2026-02-10T14:21:40.551Z", "orderId": null, "data": { "method": "PIX", "copypaste": "00020101...", "e2e": "E1823610928374650918273645091827" }, "splits": [ { "amount": 15190, "currency": "BRL", "percent": 95, "storeId": "cm5t0a9qz0000ls08p4r7c2kd", "splitId": null }, { "amount": 800, "currency": "BRL", "percent": 5, "storeId": "cm5t0c2wx0002ls08m6k9h3pb", "splitId": "cm5t0d4yz0004ls08s1v7n2qf" } ], "delivery": null, "items": [{ "quantity": 1, "name": "Product", "price": 15990, "type": "DIGITAL" }] } ``` Status codes: 200, 400 (invalid id), 401, 404 (code payment.notFound.default), 429 ### POST /v1/payment/:id/refund Refunds the full amount of a PAID payment. No request body required. The amount comes out of the store balance, which must cover the whole payment. Response (200): ```json { "success": true } ``` Status codes: 200, 400 (invalid id), 401, 404 (code payment.notFound.default), 422 (payment.businessLogic.cantRefundStatus: not PAID or a refund already in progress; payment.businessLogic.cantRefundBalance: store balance does not cover the amount), 429, 502 ## Card Tokenization Before creating a credit card payment, tokenize the card. Tokens use NaCl crypto_box_seal encryption and are generated using the tokenization key, not the API key. The tokenization key is publishable and grants no API access; the API key must never reach the browser. Both are available under Finance → Integrations, in the API Credentials section. Method 1, HTML Script Tag (frontend-side): The /v1/scripts endpoint (tenant base URL) serves an ES module, so load it with type="module". It registers the tokenizer in window["Boop"]; call init/encrypt from another module script placed after it. Card fields for this method: number, holder, expMonth (two-digit string, e.g. "08"), expYear (four-digit string, e.g. "2027"), cvv. ```html ``` Method 2, PaymentTokenizer Class (backend-side): Implement using libsodium. Card fields: number, holder, expMonth (MM), expYear (YYYY), cvv. ```ts import sodium from "https://cdn.jsdelivr.net/npm/libsodium-wrappers@0.8.0/+esm"; type CardDTO = { number: string; holder: string; expMonth: string; expYear: string; cvv: string; }; export class PaymentError extends Error { code: string; description: string; field?: string; constructor(code: string, description: string, field?: string) { super(code); this.name = "PaymentError"; this.code = code; this.description = description; this.field = field; } } export class PaymentTokenizer { tokenizationKey = ""; initialized = false; async init(pk: string): Promise { this.assertValidTokenizationKey(pk); await sodium.ready; this.tokenizationKey = pk; this.initialized = true; } async encrypt(card: CardDTO): Promise { if (!this.initialized || !this.tokenizationKey) { throw new PaymentError("TOKENIZER_NOT_INITIALIZED", "Call init(pk) before encrypt()."); } const payload = this.buildPayload(card); const encrypted = sodium.crypto_box_seal(sodium.from_string(JSON.stringify(payload)), sodium.from_base64(this.tokenizationKey)); return sodium.to_base64(encrypted); } buildPayload(card: CardDTO): CardDTO { const number = this.validateCardNumber(card.number); const holder = this.validateHolder(card.holder); const expMonth = this.validateMonth(card.expMonth); return { number, holder, expMonth, expYear: this.validateYear(card.expYear, expMonth), cvv: this.validateCvv(card.cvv), }; } assertValidTokenizationKey(pk: string) { if (!pk || typeof pk !== "string" || pk.trim() === "") { throw new PaymentError("INVALID_TOKENIZATION_KEY", "Tokenization key is missing or invalid. Use the card key from your dashboard."); } } validateHolder(value: string): string { if (!value?.trim()) { throw new PaymentError("INVALID_CARD_HOLDER", "Cardholder name is required.", "holder"); } return value.trim(); } validateCardNumber(value: string): string { const number = String(value ?? "").replace(/\D/g, ""); if (!/^\d{13,19}$/.test(number)) { throw new PaymentError("INVALID_CARD_NUMBER", "Card number must have 13 to 19 digits.", "number"); } return number; } validateMonth(value: string): string { const monthStr = String(value ?? "").replace(/\D/g, ""); if (!/^\d{2}$/.test(monthStr)) { throw new PaymentError("INVALID_EXP_MONTH", "Expiration month must be two digits between 01 and 12.", "expMonth"); } const month = Number(monthStr); if (month < 1 || month > 12) { throw new PaymentError("INVALID_EXP_MONTH", "Expiration month must be two digits between 01 and 12.", "expMonth"); } return monthStr; } validateYear(value: string, month: string): string { const yearStr = String(value ?? "").replace(/\D/g, ""); if (!/^\d{4}$/.test(yearStr)) { throw new PaymentError("INVALID_EXP_YEAR", "Expiration year must have four digits.", "expYear"); } const year = Number(yearStr); const now = new Date(); const currentYear = now.getFullYear(); const isPastYear = year < currentYear; const isPastMonth = year === currentYear && Number(month) < now.getMonth() + 1; if (isPastYear || isPastMonth) { throw new PaymentError("CARD_EXPIRED", "Card is expired.", "expYear"); } return yearStr; } validateCvv(value: string): string { const cvv = String(value ?? "").replace(/\D/g, ""); if (!/^\d{3,4}$/.test(cvv)) { throw new PaymentError("INVALID_CVV", "CVV must have 3 or 4 digits.", "cvv"); } return cvv; } } ``` How to use? ```ts const tokenizer = new PaymentTokenizer(); await tokenizer.init("CHAVE_DE_TOKENIZACAO"); const token = await tokenizer.encrypt({ number: "4111111111111111", holder: "JOHN DOE", expMonth: "12", expYear: "2030", cvv: "123" }); ``` Using the token ```ts { "method": "CREDIT_CARD", "card": { "token": "", "installments": 1 } } ``` ## Card tokenization errors Both methods throw a PaymentError with name "PaymentError", code, description and field. error.message is the code. field is the card field that failed, and is empty when the error is not about one field. | code | field | description | |---|---|---| | INVALID_TOKENIZATION_KEY | | Tokenization key is missing or invalid. Use the card key from your dashboard. | | TOKENIZER_NOT_INITIALIZED | | Call init(pk) before encrypt(). | | INVALID_CARD_HOLDER | holder | Cardholder name is required. | | INVALID_CARD_NUMBER | number | Card number must have 13 to 19 digits. | | INVALID_EXP_MONTH | expMonth | Expiration month must be two digits between 01 and 12. | | INVALID_EXP_YEAR | expYear | Expiration year must have four digits. | | CARD_EXPIRED | expYear | Card is expired. | | INVALID_CVV | cvv | CVV must have 3 or 4 digits. | ## 3DS SDK Some cards need buyer authentication (3D Secure) in the browser. Use this SDK with the tokenizer when the card form is on your page. Load it after the tokenizer script: ```html ``` It registers the SDK in window["BoopThreeDS"]. It uses the tokenization key, never the API key. Flow, steps 1 and 2 when the payment page opens, steps 3 onwards on the pay click. For some cards, prepare() starts collecting the buyer's device data for fraud analysis in the background, which takes a few seconds: if start() comes right after prepare(), the sale goes on without that data and is more likely to be declined. 1. await tokenizer.init(pk), then await threeDS.init(pk) 2. await threeDS.prepare(sale). sale has the same shape as the POST /v1/payment body, without card.token. Call it again when the amount, the currency, the installments or the address change, and when you pass another sale object: start() uses the sale from the last prepare(). The payer can be filled in after prepare(), in the same object. 3. const threeDSResult = await threeDS.start(card). It returns an object with the authentication result and the fraud analysis data, or undefined. 4. const token = await tokenizer.encrypt(card). The token carries only the card. 5. The page sends { token, threeDS: threeDSResult } to your backend. Your backend calls POST /v1/payment with card: { token, installments, billingAddress, threeDS } and returns the whole response to the page, without removing fields. 6. await threeDS.finish(paymentResponse) ```js const payButton = document.querySelector("#pay"); // when the payment page opens try { await tokenizer.init(pk); await threeDS.init(pk); await threeDS.prepare(sale); payButton.disabled = false; // the