# Health Check Source: https://docs.trymaven.com/api-reference/health/health-check /openapi.json get /health Health check endpoint with service connectivity verification. # API Reference Source: https://docs.trymaven.com/api-reference/overview Complete reference for the Maven REST API # API Reference The Maven API is a REST API that uses JSON request and response bodies. All endpoints are served over HTTPS. ## Base URL ``` https://api.trymaven.com ``` ## Authentication All endpoints require an API key passed as a Bearer token in the `Authorization` header: ```bash theme={"dark"} Authorization: Bearer mvn_test_your_key_here ``` See [Authentication](/authentication) for details on key formats and test vs live modes. ## Endpoints ### Voice Sessions Endpoints for payments collected over phone calls: | Method | Endpoint | Description | | ------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `POST` | `/v1/sessions` | Create a new voice payment session | | `GET` | `/v1/sessions` | Get session by caller phone | | `GET` | `/v1/sessions/{session_id}` | Get session by ID — status + processor details, for **voice and chat**. Use this for status checks and missed-webhook reconciliation. | | `POST` | `/v1/sessions/{session_id}/cancel` | Cancel a session | ### Widget Sessions (Chat / Web) Endpoints for payments collected via the embeddable widget. See the [Chat Widget quickstart](/widget/quickstart) for the full integration flow. | Method | Endpoint | Description | | ------ | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST` | `/v1/widget-sessions` | Create a new chat/web payment session | | `GET` | `/v1/widget-sessions/{session_id}` | Iframe-internal session details (unauthenticated). Returns `410 Gone` once terminal — **not for reconciliation**; use `GET /v1/sessions/{session_id}` instead. | | `POST` | `/v1/widget-sessions/{session_id}/charge` | Submit card data — called internally by the iframe, not by merchants | ### Auth | Method | Endpoint | Description | | ------ | ---------------- | ----------------------- | | `GET` | `/v1/auth/check` | Verify API key validity | ### Apps | Method | Endpoint | Description | | ------ | -------------- | ---------------------- | | `GET` | `/v1/projects` | List organization apps | ### Health | Method | Endpoint | Description | | ------ | --------- | -------------------- | | `GET` | `/health` | Service health check | ## Rate Limits All endpoints are rate-limited per IP address. ## Errors The API uses standard HTTP status codes. Error responses include a `detail` field with a human-readable message. ## Interactive Playground Use the API playground on each endpoint page to make test requests directly from the documentation. Enter your test API key to get started. # Cancel Session Source: https://docs.trymaven.com/api-reference/public-api-v1/cancel-session /openapi.json post /v1/sessions/{session_id}/cancel Cancel a pending session. Only sessions in PENDING or COLLECTING status can be cancelled. **Authentication:** API Key (Bearer token) # Check Auth Source: https://docs.trymaven.com/api-reference/public-api-v1/check-auth /openapi.json get /v1/auth/check Check if API key is valid. Use this to verify your API key is working correctly. **Authentication:** API Key (Bearer token) # Create Session Source: https://docs.trymaven.com/api-reference/public-api-v1/create-session /openapi.json post /v1/sessions Create a new payment session. Returns minimal identifiers needed for polling. Pass `?response_status=200` if your platform does not accept HTTP 201. **Authentication:** API Key (Bearer token) # Get Session Source: https://docs.trymaven.com/api-reference/public-api-v1/get-session /openapi.json get /v1/sessions/{session_id} Get session details by ID. Returns full session details including status, payment info, and processor details. **Authentication:** API Key (Bearer token) # Get Session By Caller Source: https://docs.trymaven.com/api-reference/public-api-v1/get-session-by-caller /openapi.json get /v1/sessions Get the most recent session for a phone number. Returns the most recent active session, or falls back to the latest terminal session. **Authentication:** API Key (Bearer token) # List Projects Source: https://docs.trymaven.com/api-reference/public-api-v1/list-projects /openapi.json get /v1/projects List all projects for this organization. **Authentication:** API Key (Bearer token) # Create a chat payment session Source: https://docs.trymaven.com/api-reference/widget-sessions/create-a-chat-payment-session /openapi.json post /v1/widget-sessions Create a chat/web payment session. Call this from your **server** with your secret API key. Hand the returned `session_id` to your frontend and mount the widget: `Maven.createPayment({ sessionId }).mount("#slot")`. Theme, labels, and fields are set once per project in the dashboard (Chat Payments tab → Customize) and applied automatically. # Authentication Source: https://docs.trymaven.com/authentication API key formats, webhook tokens, and authentication methods All Maven API requests are authenticated with API keys passed as Bearer tokens. ## API Key Format ``` mvn_{mode}_{secret} ``` | Component | Description | | --------- | ----------------------------------------------------------------- | | `mvn` | Fixed prefix | | `mode` | `test` or `live` -- determines which gateway credentials are used | | `secret` | 32-character random string | **Examples:** * Test: `mvn_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6` * Live: `mvn_live_q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2` ## Usage Pass your API key as a Bearer token in the `Authorization` header: ```bash theme={"dark"} curl -X GET https://api.trymaven.com/v1/auth/check \ -H "Authorization: Bearer mvn_test_your_key_here" ``` ## Test vs Live Mode | Feature | Test (`mvn_test_`) | Live (`mvn_live_`) | | ------------------- | ------------------ | ----------------------- | | Gateway credentials | Test/sandbox | Production | | Real charges | No | Yes | | Card validation | Full | Full | | Voice calls | Real calls placed | Real calls placed | | Rate limits | Same as live | 20 req/min per endpoint | Test mode places real calls. Use your own phone number as the caller during testing. ## Key Management Manage API keys in the [Maven Dashboard](https://app.trymaven.com) under **Settings > API Keys**. Click **Create Key** and select the mode (test or live). Copy the full key immediately -- it will only be shown once. The key is hashed server-side (SHA-256). If you lose it, revoke and create a new one. ## Error Responses | Status Code | Meaning | | ----------- | ------------------------------------------------------------- | | `401` | Invalid API key, expired key, or missing Authorization header | | `403` | Key is valid but lacks required scopes | | `429` | Rate limit exceeded | # Authorize.net Setup Source: https://docs.trymaven.com/integrations/authorizenet-setup Connect your Authorize.net account to Maven # Connecting Authorize.net Maven connects to Authorize.net using your API Login ID and Transaction Key. These credentials allow Maven to create charges and customer profiles on your behalf. ## Prerequisites * An Authorize.net merchant account * API Login ID and Transaction Key from your Authorize.net account * A Maven app ## Getting Your Credentials Go to [Authorize.net Merchant Interface](https://account.authorize.net/) and log in. Go to **Account > Settings > API Credentials & Keys**. Your **API Login ID** is displayed at the top of the page. Copy it. Under **Create New Key(s)**, select **New Transaction Key** and click **Submit**. Copy the generated key — it won't be shown again. ## Connecting in Maven In the [Maven Dashboard](https://app.trymaven.com), navigate to your app and click the **Payments** tab. Click the Authorize.net card and enter your credentials. * **API Login ID**: Your Authorize.net API Login ID * **Transaction Key**: Your Authorize.net Transaction Key Click **Connect**. Maven will validate the credentials and save them. ## Sandbox Testing For testing, use [Authorize.net Sandbox](https://sandbox.authorize.net/) credentials. Create a sandbox account at [developer.authorize.net](https://developer.authorize.net/). Use sandbox credentials with `mvn_test_` API keys to test without real charges. Sandbox responses include real `transaction_id`, `auth_code`, AVS, CVV, CAVV, and network transaction ID values, so the payload your code receives in test mode matches what you'll see in production. ## Capture Mode Maven supports two capture modes for Authorize.net charges. The setting is per-app and can be changed any time from the **Payments** tab on the Authorize.net card. | Mode | Behavior | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Auto Capture** (default) | Maven sends `authCaptureTransaction`. Funds are authorized and captured immediately. This is the standard flow. | | **Authorize Only** | Maven sends `authOnlyTransaction`. Funds are placed on hold but **not** captured. You settle the charge later from your Authorize.net dashboard or via your own `priorAuthCaptureTransaction` API call, using the `transaction_id` Maven returns in the webhook. | **When to use Authorize Only:** * Hotels, car rentals, and other businesses that authorize on booking and capture on checkout * Service businesses where the final amount may change before fulfillment * Any flow where you want to verify the card and reserve funds without immediately moving money **Important notes:** * Authorizations typically expire **30 days** after creation if not captured. Holds beyond that are released. * Once captured, the captured amount is locked and cannot be increased. * You can capture for **less than** the original auth amount (partial capture). * Maven does not currently send a follow-up webhook when you capture in Authorize.net — you track captures on your side. When Authorize Only is enabled, the session reaches the new terminal status `payment-authorized` instead of `payment-success`, and the webhook payload includes `processor.auth_only: true`. ## Processor Response Fields ### Charge Mode | Field | Description | | ------------------ | ---------------------------------------------------------------------------------------------------- | | `transaction_id` | Authorize.net transaction ID. In Authorize Only mode, use this as the `refTransId` to capture later. | | `auth_code` | Authorization code from the issuing bank | | `response_code` | Authorize.net response code (`1`=Approved, `2`=Declined, `3`=Error, `4`=Held for review) | | `avs_result_code` | AVS (Address Verification System) match result | | `cvv_result_code` | CVV match result | | `cavv_result_code` | CAVV (3D Secure) result | | `network_trans_id` | Card network transaction ID (Visa/Mastercard) — used for card-on-file flows and certain refunds | | `auth_only` | `true` when the charge was made in Authorize Only mode (auth without capture) | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | ### Result Code Cheat Sheet **`response_code`** — overall transaction outcome: | Code | Meaning | | ---- | ---------------------------- | | `1` | Approved | | `2` | Declined | | `3` | Error | | `4` | Held for review (FDS filter) | **`avs_result_code`** — billing address verification: | Code | Meaning | | ---- | ----------------------------------- | | `Y` | Address and zip both matched (best) | | `A` | Address matched, zip didn't | | `Z` | Zip matched, address didn't | | `N` | Neither matched | | `U` | AVS unavailable | | `P` | Not applicable for this card type | ### Improving AVS Coverage By default, voice sessions only collect the ZIP code from the caller — so AVS returns `Z` (zip match only) or `U` (unavailable) when no ZIP was captured. Strict Authnet AVS reject filters often decline both, which can block legitimate transactions. To get a full AVS match (`Y`), pass a `billing_address` block on session creation. We forward it to Authnet's `billTo` on both charge and tokenize paths: ```bash theme={"dark"} POST /v1/sessions Authorization: Bearer mvn_live_xxx Content-Type: application/json { "project": "my-store", "caller": "+14155551234", "amount": "49.99", "mode": "charge", "gateway": "authorizenet", "billing_address": { "street": "123 Main St", "city": "Austin", "state": "TX", "zip": "78701", "country": "US" } } ``` Always pass the cardholder's **billing** address — the one on file with the card issuer — not the shipping address. AVS only matches against billing. If you reuse a shipping address that differs from billing, AVS will return `N` (no match), which most fraud filters reject. `first_name` and `last_name` on the `billing_address` are optional — when omitted, we fall back to the cardholder name collected during card capture. See the [API reference for `POST /v1/sessions`](/api-reference/POST/v1/sessions) for the full field list and validation rules. #### Tokenize mode (CIM) When you pass `billing_address` with `mode: "tokenize"`, the address is stored on the resulting CIM Payment Profile. Subsequent charges against that profile (via `createTransactionFromProfile`) inherit the billing address automatically — no need to re-send it on the charge call, unless you explicitly override `billTo` in your charge request. **`cvv_result_code`** — CVV match: | Code | Meaning | | ---- | ------------------------------------------ | | `M` | Match | | `N` | No match | | `P` | Not processed | | `S` | Should be on the card but wasn't submitted | | `U` | Issuer doesn't support CVV verification | **`cavv_result_code`** — 3D Secure / cardholder authentication: | Code | Meaning | | ------- | ------------------------------------ | | (blank) | CAVV not validated | | `0` | Not validated, possibly not supplied | | `1` | Failed validation | | `2` | Passed validation | | `3`/`4` | Could not be performed | ### Tokenize Mode (CIM) | Field | Description | | ----------------------------- | ----------------------- | | `authnet_customer_profile_id` | CIM Customer Profile ID | | `authnet_payment_profile_id` | CIM Payment Profile ID | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | ## Using Tokenized Cards After tokenizing, use the CIM profile IDs to create transactions via the Authorize.net API: ```python theme={"dark"} # Create a charge using CIM profile from authorizenet import apicontractsv1 as api transaction = api.createTransactionRequest() transaction.transactionRequest.transactionType = "authCaptureTransaction" transaction.transactionRequest.amount = "49.99" transaction.transactionRequest.profile = api.customerProfilePaymentType() transaction.transactionRequest.profile.customerProfileId = "123456789" # from authnet_customer_profile_id transaction.transactionRequest.profile.paymentProfile = api.paymentProfile() transaction.transactionRequest.profile.paymentProfile.paymentProfileId = "987654321" # from authnet_payment_profile_id ``` # Braintree Setup Source: https://docs.trymaven.com/integrations/braintree-setup Connect your Braintree account to Maven # Connecting Braintree Maven connects to Braintree using your Merchant ID, Public Key, and Private Key. These credentials allow Maven to create transactions and vault customers on your behalf. ## Prerequisites * A Braintree merchant account * Merchant ID, Public Key, and Private Key * A Maven app ## Getting Your Credentials Go to [Braintree Control Panel](https://www.braintreegateway.com/login) and log in. Click the **gear icon** (Settings) in the top right, then click **API**. Under **API Keys**, you'll see your: * **Merchant ID** * **Public Key** * **Private Key** (click "View" to reveal) ## Connecting in Maven In the [Maven Dashboard](https://app.trymaven.com), navigate to your app and click the **Payments** tab. Click the Braintree card and enter your credentials. * **Merchant ID**: Your Braintree Merchant ID * **Public Key**: Your Braintree Public Key * **Private Key**: Your Braintree Private Key Click **Connect**. Maven will validate the credentials and save them. ## Sandbox Testing For testing, use [Braintree Sandbox](https://sandbox.braintreegateway.com/) credentials. Create a sandbox account at [braintreepayments.com/sandbox](https://www.braintreepayments.com/sandbox). Use sandbox credentials with `mvn_test_` API keys to test without real charges. ## Processor Response Fields ### Charge Mode | Field | Description | | -------------------------------- | -------------------- | | `braintree_transaction_id` | Transaction ID | | `braintree_customer_id` | Customer ID | | `braintree_payment_method_token` | Payment method token | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | ### Tokenize Mode (Vault) | Field | Description | | -------------------------------- | -------------------- | | `braintree_customer_id` | Vault Customer ID | | `braintree_payment_method_token` | Payment Method Token | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | ## Using Tokenized Cards After tokenizing, use the vault token to create transactions via the Braintree SDK: ```python theme={"dark"} import braintree result = braintree.Transaction.sale({ "amount": "49.99", "payment_method_token": "abc123", # from braintree_payment_method_token "options": { "submit_for_settlement": True, }, }) ``` # Card Forwarding Source: https://docs.trymaven.com/integrations/card-forwarding Receive collected card data at your own PCI-compliant endpoint and process it with any processor # Card Forwarding (Webhook Gateway) If your organization is **PCI-DSS compliant** and runs its own processor integration, Maven doesn't have to charge the card at all. With the `webhook` gateway, Maven collects the card over the phone call, then POSTs the full card data to your HTTPS endpoint. Your synchronous response decides whether the session succeeds — you charge (or vault) the card with any processor you like. Your receiving endpoint takes full cardholder data (PAN, expiry, CVV) into your PCI scope. Only use this integration if your organization is PCI-DSS compliant for handling CHD. Never store the CVV. ## How It Works 1. You create a session with `"gateway": "webhook"`. 2. The caller enters their card on the phone as usual. 3. Maven POSTs the card data to your configured endpoint — signed, HTTPS-only, exactly once. 4. Your endpoint charges/vaults the card with your processor and responds `success: true` or `success: false`. 5. Maven finishes the call accordingly and fires the normal (card-free) status webhook. ## Setup In the [Maven Dashboard](https://app.trymaven.com), open your app and click the **Payments** tab. Click the **Card Forwarding** card to expand it. Enter your HTTPS endpoint URL per environment (Test / Live). HTTP URLs are rejected. Test-mode API keys (`mvn_test_`) forward to your test endpoint; live keys to your live endpoint. On first save you receive a **signing secret** (`whsec_…`) — it is shown only once. Use it to verify the `Maven-Signature` header on every forwarded payload. ## Creating Sessions ```bash theme={"dark"} curl -X POST https://api.trymaven.com/v1/sessions \ -H "Authorization: Bearer mvn_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "project": "my-store", "caller": "+14155551234", "amount": 25.99, "mode": "charge", "gateway": "webhook" }' ``` ## The Forwarded Request When the caller finishes entering their card, Maven POSTs to your endpoint: ``` POST Content-Type: application/json Maven-Signature: t=1725370000,v1= ``` ```json theme={"dark"} { "type": "payment_method.collected", "session_id": "a1b2c3d4-...", "environment": "test", "mode": "charge", "amount": 2599, "currency": "USD", "description": "Order #1042", "caller": "+14155551234", "card": { "number": "4111111111111111", "exp_month": "05", "exp_year": "2027", "cvv": "123", "cardholder_name": "Jane Doe", "postal_code": "90210", "brand": "visa", "last4": "1111" } } ``` | Field | Description | | ------------- | ----------------------------------------------------------------------------------------------------- | | `type` | Always `"payment_method.collected"` | | `session_id` | Session UUID — use as your idempotency key | | `environment` | `"test"` or `"live"` | | `mode` | `"charge"` or `"tokenize"` — in tokenize mode, vault the card in your own system and return a `token` | | `amount` | Amount in **minor units** (cents) — note this differs from the status webhook, which uses dollars | | `card` | Full card data. Fields that weren't collected (e.g. `cardholder_name`, `postal_code`) are `null` | ### Verifying the Signature Card-forwarding requests are signed with your card-forwarding secret using the same scheme as [status webhooks](/integrations/webhooks#verifying-webhook-signatures): ``` signed_content = "{t}." + raw_request_body expected = hex(hmac_sha256(secret, signed_content)) ``` Compare `expected` to the `v1` value with a constant-time comparison, and reject requests whose `t` timestamp is older than your tolerance (e.g. 5 minutes). The card-forwarding secret (`whsec_…`, shown once when you save the endpoint) is separate from your project's status-webhook secret. ## Your Response Respond within **30 seconds**. Maven makes exactly **one** attempt per session — it never retries, so you can't be double-charged by retry storms. Still, treat `session_id` as your idempotency key. Approve (HTTP 200): ```json theme={"dark"} { "success": true, "transaction_id": "qp_12345", "token": "tok_abc" } ``` Decline (HTTP 200): ```json theme={"dark"} { "success": false, "error_code": "card_declined", "error_message": "Insufficient funds" } ``` | Field | Description | | ------------------------------ | ------------------------------------------------------------------------ | | `success` | Required. `true` completes the session; `false` fails it | | `transaction_id` | Optional. Your processor's transaction ID — stored on the session | | `token` | Optional (tokenize mode). Your vault token — stored on the session | | `error_code` / `error_message` | Optional on declines — surfaced in the session status and status webhook | Any non-2xx status, invalid JSON, or timeout fails the session with `webhook_gateway_error` and the caller hears the failure prompt. ## Processor Response Fields Your returned IDs are echoed on the session (`GET /v1/sessions/{id}` → `processor`) and in the status webhook: | Field | Description | | ------------------------ | -------------------------------------------------- | | `webhook_transaction_id` | The `transaction_id` your endpoint returned | | `webhook_token` | The `token` your endpoint returned (tokenize mode) | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | ## Security Guarantees * **HTTPS enforced** at save time and again at send time; HTTP endpoints are rejected. * **Redirects are never followed** — card data can only reach the exact host you configured. * **Exactly one delivery attempt** per session — no retry double-charges. * **Every payload is signed** so you can prove the request came from Maven. * Maven **never stores or logs** the card data it forwards — only your returned IDs are persisted. # Custom Voice Platform Source: https://docs.trymaven.com/integrations/custom-platform Integrate Maven voice payments with any Twilio-based or SIP-capable voice agent platform # Custom Voice Platform Integration Add PCI-compliant voice payments to any voice agent platform — Outbox AI, Bland, or any custom Twilio-based system. This guide covers the universal integration pattern that works with any platform capable of HTTP tool calls and call transfers. ## How It Works During a call, your voice agent calls the Maven API to create a payment session with the amount and caller's phone number. Maven creates a session and returns a phone number (and SIP URI) to transfer the caller to. Your agent transfers the live call to Maven's secure payment line. **The transfer must preserve the original caller's phone number.** Maven matches sessions by caller ID. If your platform replaces the caller ID with its own trunk number during transfer, the session will silently fail to connect. See [Caller ID Preservation](#caller-id-preservation) below. Maven collects the card details over voice, processes the payment, and sends a [webhook](/integrations/webhooks) with the result. The caller is optionally transferred back to your agent via the `callback` number. ## Prerequisites 1. A Maven account with an [API key](/authentication) (`mvn_test_` for test mode, `mvn_live_` for production) 2. An app with a [payment gateway connected](/quickstart) (Stripe, Authorize.net, Braintree, Shift4, or Fiserv) 3. A voice agent platform that supports HTTP tool calls and call transfers ## Step 1 — Create a Payment Session When your agent decides to collect a payment, call the Maven API: ```bash theme={"dark"} curl -X POST https://api.trymaven.com/v1/sessions \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "project": "your-app-slug", "caller": "+14155551234", "amount": 150.00, "gateway": "stripe", "mode": "charge", "description": "Invoice #1234", "callback": "+18005550000" }' ``` **Response (HTTP 201):** ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "created", "phone_number": "+18338..." , "sip_uri": "sip:a1b2c3d4@sip.trymaven.com", "created_at": "2026-04-26T12:00:00Z" } ``` | Field | Description | | ------------- | -------------------------------------------------------------------------------------------------------------------------- | | `project` | Your app slug from the Maven Dashboard | | `caller` | The customer's phone number in E.164 format. **Must match the caller ID on the transferred call.** | | `amount` | Payment amount in dollars (e.g. `150.00`) | | `gateway` | `stripe`, `authorizenet`, `braintree`, `shift4`, `fiserv`, `jpmorgan`, or `webhook` (card forwarding to your own endpoint) | | `mode` | `charge` (process immediately) or `tokenize` (save card only) | | `description` | Optional — what the payment is for | | `callback` | Optional — phone number or SIP URI to transfer the caller back to after payment | ### HTTP 201 Compatibility Some platforms (e.g. Outbox AI) only accept HTTP 200 as a success response. If your platform wraps non-200 responses as errors, add `?response_status=200` to force a 200 response: ``` POST https://api.trymaven.com/v1/sessions?response_status=200 ``` The response body is identical — only the status code changes. ## Step 2 — Transfer the Call After creating the session, transfer the caller to the `phone_number` (PSTN) or `sip_uri` (SIP) returned in the response. Use a **cold transfer** — Maven handles the entire payment conversation. ### PSTN Transfer Transfer to the `phone_number` field. This works on any platform that supports standard call transfers. ### SIP Transfer Transfer to the `sip_uri` field. SIP transfers are more reliable for caller ID preservation because you can pass identifying headers: * `X-Session-Id` — The session UUID (highest priority match, skips caller ID lookup entirely) * `X-Caller` — The original caller's phone number (useful if the platform can't preserve caller ID natively) If your platform supports SIP headers, pass `X-Session-Id` on the transfer. This is the most reliable matching method — it doesn't depend on caller ID at all. ## Step 3 — Get the Result Maven sends a [webhook](/integrations/webhooks) to your app's webhook URL when the session completes. You can also poll the session status: ```bash theme={"dark"} # By session ID curl https://api.trymaven.com/v1/sessions/SESSION_ID \ -H "Authorization: Bearer YOUR_API_KEY" # By caller phone number (returns most recent session) curl "https://api.trymaven.com/v1/sessions?caller=%2B14155551234" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Caller ID Preservation This is the most common integration pitfall. Maven matches inbound calls to pending sessions by the caller's phone number. If the caller ID doesn't match, the session won't connect. ### How matching works When Maven receives a transferred call, it checks these fields in priority order: 1. `X-Session-Id` SIP header (direct UUID match — **most reliable**) 2. `X-Caller` SIP header (phone number override) 3. SIP `To` URI (if it contains the session ID or phone number) 4. `From` field (the caller ID on the inbound leg) ### Common pitfalls | Platform behavior | Result | Fix | | ----------------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------- | | Platform replaces caller ID with its own trunk number | Session doesn't match | Use SIP transfer with `X-Session-Id` header | | Platform uses agent's number as caller ID | Session doesn't match | Configure transfer to use **customer's number** as displayed caller ID | | Platform strips SIP headers | Falls back to `From` field matching | Ensure PSTN caller ID is preserved | ### Diagnosing caller ID issues If sessions are created but calls don't connect, verify the session exists for the expected caller: ```bash theme={"dark"} curl "https://api.trymaven.com/v1/sessions?caller=%2B14155551234" \ -H "Authorization: Bearer YOUR_API_KEY" ``` If this returns the session, the issue is that the transferred call's `From` doesn't match `+14155551234`. Check your platform's transfer settings. ## Tool Configuration Most voice agent platforms let you define HTTP tools. Here's how to configure them: ### collect\_payment tool | Setting | Value | | ----------- | ---------------------------------------------------------------------- | | **Method** | `POST` | | **URL** | `https://api.trymaven.com/v1/sessions?response_status=200` | | **Headers** | `Authorization: Bearer YOUR_API_KEY`, `Content-Type: application/json` | **Parameters:** ```json theme={"dark"} { "type": "object", "properties": { "amount": { "type": "number", "description": "Payment amount in dollars (e.g. 150.00)" }, "caller": { "type": "string", "description": "Customer phone number in E.164 format (e.g. +14155551234)" }, "description": { "type": "string", "description": "What the payment is for" }, "callback": { "type": "string", "description": "Phone number to transfer the caller back to after payment" } }, "required": ["amount", "caller"] } ``` The `project`, `gateway`, and `mode` fields should be hardcoded in the URL or request body rather than exposed to the LLM. Use the URL format: ``` POST https://api.trymaven.com/v1/sessions?response_status=200 ``` with a fixed body that includes `"project": "your-app-slug"`, `"gateway": "stripe"`, `"mode": "charge"`. ### get\_session tool | Setting | Value | | ----------- | ------------------------------------------------------------ | | **Method** | `GET` | | **URL** | `https://api.trymaven.com/v1/sessions?caller={phone_number}` | | **Headers** | `Authorization: Bearer YOUR_API_KEY` | Use this after the caller returns from the payment line to check the result. ### cancel\_session tool | Setting | Value | | ----------- | ---------------------------------------------------------- | | **Method** | `POST` | | **URL** | `https://api.trymaven.com/v1/sessions/{session_id}/cancel` | | **Headers** | `Authorization: Bearer YOUR_API_KEY` | ## Example Agent Prompt Add something like this to your voice agent's system prompt: ``` When a caller needs to make a payment: 1. Confirm the amount with the caller 2. Call collect_payment with: - amount: the payment amount - caller: the customer's phone number - description: a short description of the payment - callback: your agent's phone number (so the caller returns after payment) 3. Tell the caller: "I'm transferring you to our secure payment line now." 4. IMMEDIATELY transfer the call to the phone_number returned by collect_payment. Do NOT wait for the caller to confirm — transfer right away. After the caller returns from the payment line: 1. Call get_session with the caller's phone number 2. If status is "payment-success" — confirm the payment and thank them 3. If status is "payment-failed" or "expired" — let them know and offer to try again Never ask for card details yourself — the secure payment line handles that. ``` If your agent has trouble chaining the create and transfer steps, add explicit instructions like "You MUST call transfer immediately after collect\_payment succeeds — do not wait or ask for confirmation." ## Troubleshooting | Problem | Cause | Solution | | ------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------- | | Tool call returns error with `201` in the message | Platform doesn't accept HTTP 201 | Add `?response_status=200` to the URL | | Session created but call doesn't connect | Caller ID mismatch | See [Caller ID Preservation](#caller-id-preservation) | | Agent creates session but doesn't transfer | LLM not chaining tool calls | Add explicit transfer instructions to agent prompt | | Transfer connects but "no session found" | Session expired (5 min TTL) | Create the session immediately before transferring, not at the start of the call | ## Next Get notified when sessions complete. Explore the full API. Configure TTS voice and language. Test payments with test cards. # Fiserv Setup Source: https://docs.trymaven.com/integrations/fiserv-setup Connect your Fiserv Commerce Hub / Payments Live account to Maven # Connecting Fiserv Maven connects to Fiserv (Commerce Hub / Payments Live) using your **API Key** and **API Secret**. These credentials let Maven create charges or vault cards on your behalf. ## Prerequisites * A Fiserv developer account * API Key and API Secret * A Maven app ## Getting Your Credentials Go to [portal.fiserv.dev](https://portal.fiserv.dev/) and complete registration. Verify your email and set up MFA. Once logged in, go to the **API Keys** page. A pre-generated **Test Sandbox Key** is already there with access to all APIs and a dummy Store ID — no approval step needed for sandbox. You'll see: * **API Key** — sent in the `Api-Key` header on each request * **API Secret** — used as the HMAC-SHA256 secret to sign each request body For production, follow Fiserv's separate onboarding flow to get production keys plus your production base URL. ## Connecting in Maven In the [Maven Dashboard](https://app.trymaven.com), open your app and click the **Payments** tab. Click the Fiserv card to expand it, then click **Connect Fiserv**. * **Environment**: Sandbox or Production * **API Key**: Your Fiserv Api-Key * **API Secret**: Your Fiserv API Secret * **Production Base URL** (optional, production only): Override if Fiserv assigned your tenant a non-default hostname. Leave blank to use the standard hostname. Click **Save Credentials**. Maven stores them per-environment so you can have sandbox and production keys configured side by side. ### Sending the form to your customer If your customer is the merchant (i.e., the Fiserv account is theirs, not yours), click **Copy link** next to Save. That generates a single-use, 7-day link they can open in a browser and paste their credentials directly into a hosted form. Their keys are written to your project without you ever seeing them. ## Sandbox Testing The portal.fiserv.dev sandbox key works immediately on signup — no approval. Use it with `mvn_test_` API keys to test without real charges. A useful sandbox test card: | Field | Value | | ------ | --------------------- | | Number | `4035 8740 0042 4977` | | Expiry | Any future date | | CVV | `977` | ## Processor Response Fields ### Charge Mode | Field | Description | | ----------------------- | ---------------------------------------------------------------------------------- | | `fiserv_transaction_id` | Fiserv `ipgTransactionId` | | `fiserv_order_id` | Fiserv `orderId` | | `fiserv_payment_token` | Reusable payment token (returned on every charge — store it for future re-charges) | | `fiserv_state` | `CAPTURED` (auto) or `AUTHORIZED` (manual) | | `fiserv_status` | `APPROVED` on success | | `approval_code` | Fiserv authorization code | | `response_code` | Processor response code (`00` = success) | | `response_message` | Processor response message | | `card_brand` | Card brand (e.g. `VISA`) | | `card_last4` | Last 4 digits | ### Tokenize Mode (Vault) | Field | Description | | ---------------------- | ------------------------------------------------------- | | `fiserv_payment_token` | Reusable Fiserv payment token (UUID) | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | | `ipg_transaction_id` | Fiserv `ipgTransactionId` from the tokenization request | ## Using Tokenized Cards After tokenizing, charge the stored Fiserv `paymentToken` directly. Each request is HMAC-SHA256 signed: ```python theme={"dark"} import base64, hashlib, hmac, json, time, uuid import httpx API_KEY = "your_api_key" API_SECRET = "your_api_secret" BASE_URL = "https://prod.emea.api.fiservapps.com/sandbox/ipp/payments-gateway/v2" body = { "requestType": "PaymentTokenSaleTransaction", "transactionAmount": {"total": 49.99, "currency": "USD"}, "paymentMethod": {"paymentToken": {"value": "your-fiserv-payment-token"}}, "transactionOrigin": "ECOM", } body_str = json.dumps(body, separators=(",", ":")) cri = str(uuid.uuid4()) ts = str(int(time.time() * 1000)) raw = (API_KEY + cri + ts + body_str).encode() sig = base64.b64encode(hmac.new(API_SECRET.encode(), raw, hashlib.sha256).digest()).decode() resp = httpx.post( f"{BASE_URL}/payments", content=body_str, headers={ "Content-Type": "application/json", "Api-Key": API_KEY, "Client-Request-Id": cri, "Timestamp": ts, "Message-Signature": sig, }, ) print(resp.json()) ``` # J.P. Morgan Setup Source: https://docs.trymaven.com/integrations/jpmorgan-setup Connect your J.P. Morgan Payments (Chase) account to Maven # Connecting J.P. Morgan Maven connects to J.P. Morgan Payments (Online Payments API) using your **Client ID**, **Client Secret**, and **Merchant ID**. Maven exchanges the Client ID/Secret for short-lived OAuth bearer tokens and uses them to create charges or store cards on your behalf. ## Prerequisites * A J.P. Morgan Payments developer account * Client ID, Client Secret, and Merchant ID * A Maven app ## Getting Your Credentials Go to [developer.payments.jpmorgan.com](https://developer.payments.jpmorgan.com/) and create a developer account (email or GitHub/LinkedIn/Google login). In your workspace, create a project and add the **Online Payments API**. Sandbox credentials are issued instantly — no approval step. From the project's **Get set up** section, copy: * **Client ID** — OAuth client identifier * **Client Secret** — OAuth client secret * **Merchant ID** — sent as the `merchant-id` header on every request (the sandbox/mock environment uses `998482157630`) For production, credentials are issued by the J.P. Morgan implementations team during merchant onboarding. Onboarding is sales-led and typically takes 6–12 weeks; ask them to confirm your **production base URL** and OAuth scope at the same time. ## Connecting in Maven In the [Maven Dashboard](https://app.trymaven.com), open your app and click the **Payments** tab. Click the J.P. Morgan card to expand it, then click **Connect J.P. Morgan**. * **Environment**: Sandbox or Production * **Client ID**: Your OAuth Client ID * **Client Secret**: Your OAuth Client Secret * **Merchant ID**: Your J.P. Morgan Merchant ID * **Production Base URL** (optional, production only): Override if J.P. Morgan assigned your tenant a non-default hostname. Leave blank to use the standard hostname. Click **Save Credentials**. Maven stores them per-environment so you can have sandbox and production credentials configured side by side. ### Sending the form to your customer If your customer is the merchant (i.e., the J.P. Morgan account is theirs, not yours), click **Copy link** next to Save. That generates a single-use, 7-day link they can open in a browser and paste their credentials directly into a hosted form. Their credentials are written to your project without you ever seeing them. ## Sandbox Testing Sandbox credentials work immediately on signup against J.P. Morgan's mock environment. Use them with `mvn_test_` API keys to test without real charges. A useful sandbox test card: | Field | Value | | ------ | --------------------- | | Number | `4012 0000 3333 0026` | | Expiry | Any future date | | CVV | Any 3 digits | The J.P. Morgan mock environment approves every transaction — declines can only be exercised with the test credentials issued at merchant onboarding. ## Processor Response Fields ### Charge Mode | Field | Description | | ------------------------- | ------------------------------------------ | | `jpmorgan_transaction_id` | J.P. Morgan `transactionId` | | `jpmorgan_state` | `CLOSED` (captured) or `OPEN` (authorized) | | `jpmorgan_status` | `SUCCESS` on approval | | `approval_code` | Issuer approval code | | `response_code` | `APPROVED` on success | | `response_message` | Processor response message | | `card_brand` | Card brand (e.g. `visa`) | | `card_last4` | Last 4 digits | ### Tokenize Mode (Stored Card) In tokenize mode, Maven runs a \$0 verification and stores the card at J.P. Morgan. | Field | Description | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | | `jpmorgan_consumer_profile_id` | Consumer profile ID (when profile creation is enabled for your merchant) | | `jpmorgan_payment_method_id` | Payment method ID within the consumer profile | | `jpmorgan_payment_token` | Safetech network token — reusable as the `accountNumber` in later charges (returned when Safetech tokenization is enabled) | | `jpmorgan_transaction_id` | Verification `transactionId` | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | ## Using Stored Cards Charge a stored card directly against the J.P. Morgan API. With a consumer profile: ```python theme={"dark"} import httpx # 1. Get an OAuth token token_resp = httpx.post( "https://id.payments.jpmorgan.com/am/oauth2/alpha/access_token", data={ "grant_type": "client_credentials", "scope": "jpm:payments:sandbox", "client_id": "your_client_id", "client_secret": "your_client_secret", }, ) access_token = token_resp.json()["access_token"] # 2. Charge the stored payment method resp = httpx.post( "https://api-mock.payments.jpmorgan.com/api/v2/payments", json={ "captureMethod": "NOW", "merchantOrderNumber": "order-1042", "amount": 4999, # minor units — $49.99 "currency": "USD", "initiatorType": "CARDHOLDER", "accountOnFile": "STORED", "merchant": {"merchantSoftware": {"companyName": "YourCo", "productName": "YourApp"}}, "paymentMethodType": { "consumerProfile": { "consumerProfileId": "your-consumer-profile-id", "paymentMethodId": "your-payment-method-id", } }, }, headers={ "Authorization": f"Bearer {access_token}", "merchant-id": "your_merchant_id", "request-id": "unique-uuid-per-request", }, ) print(resp.json()) ``` With a Safetech token, send it as `paymentMethodType.card.accountNumber` instead, keeping `accountOnFile: "STORED"`. # Retell Source: https://docs.trymaven.com/integrations/retell Integrate Maven voice payments with your Retell AI agent # Retell Integration Add PCI-compliant voice payments to your Retell AI agent. When your agent needs to collect a payment, it calls Maven to create a session and transfers the caller to Maven's secure payment line. ## How It Works During a call, your Retell agent calls the `collect_payment` custom function with the amount and caller's phone number. Maven creates a payment session and returns a phone number to transfer the caller to. Your agent uses a Retell Transfer Call node to send the caller to Maven's payment line. Maven collects the card details, processes the payment, and sends a [webhook](/integrations/webhooks) with the result. The caller is optionally transferred back to your agent via the `callback` number. ## Setup Retell requires manual configuration of custom functions. Here's how to set it up: In the [Maven Dashboard](https://app.trymaven.com), go to your app's **Integrations** tab and generate a Retell webhook token. This gives you a URL like: ``` https://api.trymaven.com/integrations/retell/webhook?token=whk_xxx ``` In your Retell agent, add a **Custom Function** with: * **Name**: `collect_payment` * **URL**: Your server URL from step 1 * **Description**: `Create a secure payment session. Returns a transfer_number to transfer the caller to.` * **Parameters** (paste this JSON): ```json theme={"dark"} { "type": "object", "properties": { "amount": { "type": "number", "description": "Payment amount in dollars (e.g. 150.00)" }, "caller": { "type": "string", "description": "Customer phone number in E.164 format (e.g. +14155551234)" }, "description": { "type": "string", "description": "What the payment is for" }, "callback": { "type": "string", "description": "Phone number to transfer the caller back to after payment" }, "memory": { "type": "string", "description": "Summary of the call context to carry across the transfer" } }, "required": ["amount", "caller"] } ``` This is a **Transfer Call node**, not a custom function. In your Retell agent's conversation flow, add a Transfer Call node with: * **Routing**: Dynamic — prompt the agent: *"Transfer to the transfer\_number returned by collect\_payment."* * **Transfer type**: Cold Transfer * **Displayed Caller ID**: **User's Number** *(required — Maven matches sessions by caller ID, so this must be the caller's number, not the agent's)* You can also add these as custom functions using the same server URL: **get\_session** — Look up the most recent payment session by phone number. Use after the caller returns from the payment line. ```json theme={"dark"} { "type": "object", "properties": { "phone_number": { "type": "string", "description": "Customer phone number in E.164 format (e.g. +14155551234)" } }, "required": ["phone_number"] } ``` **cancel\_session** — Cancel a pending payment session. ```json theme={"dark"} { "type": "object", "properties": { "session_id": { "type": "string", "description": "The session ID returned by collect_payment" } }, "required": ["session_id"] } ``` ## Example System Prompt Add something like this to your Retell agent's system prompt. Retell provides `{{user_number}}` and `{{agent_number}}` as dynamic variables. ``` When a caller needs to make a payment, use the following flow: 1. Confirm the caller wants to pay their balance 2. Call collect_payment with: - amount: the payment amount - caller: {{user_number}} - description: a short description of the payment - callback: {{agent_number}} 3. Tell the caller you're transferring them to our secure payment line 4. Transfer the call to the transfer_number returned by collect_payment After the caller returns from the payment line: 1. Call get_session with {{user_number}} to check the payment status 2. If status is "payment-success" — confirm the payment was successful and thank them 3. If status is "payment-failed" or "expired" — let them know and offer to try again Keep responses short and conversational. Do not ask for card details — the secure payment line handles that. ``` # Shift4 Setup Source: https://docs.trymaven.com/integrations/shift4-setup Connect your Shift4 account to Maven # Connecting Shift4 Maven connects to Shift4 using your Public Key and Secret Key. These credentials allow Maven to create charges and vault cards on your behalf. ## Prerequisites * A Shift4 account * Public Key and Secret Key * A Maven app ## Getting Your Credentials Go to [Shift4 Dashboard](https://dev.shift4.com/) and log in. Click **API Keys** in the left sidebar. You'll see your: * **Public Key** (starts with `pk_`) * **Secret Key** (starts with `sk_`) Use the **Test** keys for sandbox testing and **Live** keys for production. ## Connecting in Maven In the [Maven Dashboard](https://app.trymaven.com), navigate to your app and click the **Payments** tab. Click the Shift4 card and enter your credentials. * **Public Key**: Your Shift4 Public Key * **Secret Key**: Your Shift4 Secret Key Click **Connect**. Maven will validate the credentials and save them. ## Sandbox Testing For testing, use your **Test** API keys from the Shift4 dashboard. Test keys start with `pk_test_` and `sk_test_`. Use test credentials with `mvn_test_` API keys to test without real charges. ## Processor Response Fields ### Charge Mode | Field | Description | | ------------- | ------------------ | | `charge_id` | Shift4 charge ID | | `customer_id` | Shift4 customer ID | | `card_id` | Shift4 card ID | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | ### Tokenize Mode (Vault) | Field | Description | | ------------- | ------------------ | | `customer_id` | Shift4 customer ID | | `card_id` | Shift4 card ID | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | ## Using Tokenized Cards After tokenizing, use the customer and card IDs to create charges via the Shift4 API: ```python theme={"dark"} import httpx resp = httpx.post( "https://api.shift4.com/charges", auth=("sk_live_your_secret_key", ""), data={ "amount": 4999, "currency": "USD", "customerId": "cust_xxx", # from customer_id "card": "card_xxx", # from card_id }, ) ``` # Stripe Connect Source: https://docs.trymaven.com/integrations/stripe-connect Connect your Stripe account to Maven via OAuth # Connecting Stripe Maven uses Stripe Connect to process payments on your Stripe account. The setup uses OAuth — you authorize Maven to create charges on your behalf. ## Prerequisites * A Stripe account (test or live) * A Maven app ## Setup Steps In the [Maven Dashboard](https://app.trymaven.com), navigate to your app and click the **Payments** tab. Click the **Connect Stripe** button. You'll be redirected to Stripe's OAuth authorization page. On the Stripe page, review the permissions and click **Connect**. Maven requests the ability to create charges and customers on your account. After authorization, you'll be redirected back to the Maven Dashboard. Your Stripe account will show as connected. ## Test vs Live Maven stores separate credentials for test and live modes: * **Test mode** (`mvn_test_` keys): Uses your Stripe test mode. No real charges. * **Live mode** (`mvn_live_` keys): Uses your Stripe live mode. Real charges. Connect both test and live Stripe accounts for full functionality. ## Processor Response Fields When a payment succeeds via Stripe, the `processor` object in the [GET session](/api-reference/overview) response includes: ### Charge Mode | Field | Description | | -------------------------- | ----------------------------------- | | `stripe_payment_intent_id` | PaymentIntent ID (e.g., `pi_xxx`) | | `stripe_charge_id` | Charge ID (e.g., `ch_xxx`) | | `receipt_url` | Stripe-hosted receipt URL | | `payment_method_id` | PaymentMethod ID (e.g., `pm_xxx`) | | `card_brand` | Card brand (visa, mastercard, etc.) | | `card_last4` | Last 4 digits | ### Tokenize Mode | Field | Description | | -------------------- | ----------------------------------------------------------------------------------------------- | | `stripe_customer_id` | Customer on **your connected account** (e.g., `cus_xxx`) | | `payment_method_id` | PaymentMethod on **your connected account**, already attached to that customer (e.g., `pm_xxx`) | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | Both `stripe_customer_id` and `payment_method_id` live on **your own connected Stripe account** — not on Maven's platform account. Query and charge them with **your own Stripe secret key** (no `stripe_account` header needed, because they're already on your account). Each tokenize creates a **dedicated customer with exactly one saved card**, so the `customer` + `payment_method` pair uniquely identifies the card — you never have to guess among multiple payment methods. ## Using Tokenized Cards After tokenizing, use the returned `stripe_customer_id` and `payment_method_id` to create charges via the Stripe API. Run this with **your own Stripe secret key** — the customer and payment method are on your account: ```python theme={"dark"} import stripe stripe.api_key = "sk_live_...your_account_key..." stripe.PaymentIntent.create( amount=4999, currency="usd", customer="cus_xxx", # from processor.stripe_customer_id payment_method="pm_xxx", # from processor.payment_method_id (on your account) confirm=True, off_session=True, ) ``` Pass **both** `customer` and `payment_method`. Stripe treats the customer as optional in general, but because the card is saved to that customer, passing it is the reliable way to charge the intended card. As an extra safeguard you can verify `card_last4` before charging. ## Disconnecting To disconnect Stripe, go to the **Payments** tab in your app and click **Disconnect**. Existing sessions won't be affected, but new sessions using the `stripe` gateway will fail. # Twilio Source: https://docs.trymaven.com/integrations/twilio Integrate Maven voice payments into your Twilio-based voice application # Twilio Integration Add PCI-compliant voice payments to a voice application you've built directly on Twilio. This guide is for developers who orchestrate their own call flow using TwiML or the Twilio REST API — if you're using a voice agent platform like VAPI, Retell, or Outbox, see the [platform-specific guides](/integrations/vapi) or [Custom Platform](/integrations/custom-platform) guide instead. ## How It Works When your call flow needs to collect a payment, your server calls the Maven API with the amount and caller's phone number. Maven returns a phone number. Your server responds with TwiML that dials that number, preserving the original caller ID. Maven handles the entire card collection conversation — card number, expiry, CVV, and ZIP code. Your app is never exposed to card data (PCI compliant). After payment, Maven transfers the caller back to your `callback` number. You receive a [webhook](/integrations/webhooks) with the payment result. ## Prerequisites 1. A Maven account with an [API key](/authentication) 2. An app with a [payment gateway connected](/quickstart) 3. A Twilio account with a phone number ## Step 1 — Create a Payment Session When your call flow reaches the payment step, create a session from your server: ```python theme={"dark"} import httpx async def create_payment_session(caller: str, amount: float, callback: str): async with httpx.AsyncClient() as client: resp = await client.post( "https://api.trymaven.com/v1/sessions", headers={"Authorization": "Bearer YOUR_API_KEY"}, json={ "project": "your-app-slug", "caller": caller, # customer's phone number (E.164) "amount": amount, "gateway": "stripe", "mode": "charge", "callback": callback, # your Twilio number to return the caller to }, ) resp.raise_for_status() return resp.json() ``` The response includes: ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "created", "phone_number": "+18338...", "created_at": "2026-04-26T12:00:00Z" } ``` ## Step 2 — Transfer with TwiML Respond to the Twilio webhook with a `` that transfers the caller to Maven's payment line. The critical detail: **set `callerId` to the customer's phone number**, not your Twilio number. ```python theme={"dark"} from twilio.twiml.voice_response import VoiceResponse def build_transfer_twiml(maven_phone: str, customer_phone: str, action_url: str): response = VoiceResponse() response.say("I'm transferring you to our secure payment line now.") dial = response.dial( caller_id=customer_phone, # preserve the original caller ID action=action_url, # called when the dial completes timeout=30, ) dial.number(maven_phone) return str(response) ``` This generates: ```xml theme={"dark"} I'm transferring you to our secure payment line now. +18338... ``` **`callerId` must be the customer's phone number.** Maven matches sessions by the caller ID on the inbound leg. If you use your Twilio number as the caller ID, the session won't connect. ## Step 3 — Handle the Result After the payment completes and the caller is transferred back, Twilio hits your `action` URL. You can look up the session result: ```python theme={"dark"} async def handle_payment_complete(caller: str): async with httpx.AsyncClient() as client: resp = await client.get( "https://api.trymaven.com/v1/sessions", headers={"Authorization": "Bearer YOUR_API_KEY"}, params={"caller": caller}, ) session = resp.json() if session["status"] == "payment-success": # payment went through return build_twiml_say("Your payment was successful. Thank you!") elif session["status"] == "payment-failed": return build_twiml_say("The payment didn't go through. Would you like to try again?") else: return build_twiml_say("It looks like the payment session expired.") ``` You'll also receive a [webhook](/integrations/webhooks) with the full payment details (card brand, last 4, gateway transaction IDs). ## Full Example Here's a minimal FastAPI app that handles the complete flow: ```python theme={"dark"} from fastapi import FastAPI, Form, Request from fastapi.responses import Response from twilio.twiml.voice_response import VoiceResponse import httpx app = FastAPI() MAVEN_API_KEY = "mvn_test_..." MAVEN_PROJECT = "your-app-slug" YOUR_TWILIO_NUMBER = "+18005550000" API_BASE = "https://your-server.com" @app.post("/incoming-call") async def incoming_call(From: str = Form(...)): """Twilio hits this when a call comes in.""" response = VoiceResponse() response.say("Welcome! Let me collect your payment.") response.redirect(f"{API_BASE}/start-payment?caller={From}") return Response(content=str(response), media_type="text/xml") @app.post("/start-payment") async def start_payment(caller: str): """Create a Maven session and transfer the caller.""" # 1. Create the payment session async with httpx.AsyncClient() as client: resp = await client.post( "https://api.trymaven.com/v1/sessions", headers={"Authorization": f"Bearer {MAVEN_API_KEY}"}, json={ "project": MAVEN_PROJECT, "caller": caller, "amount": 49.99, "gateway": "stripe", "mode": "charge", "callback": YOUR_TWILIO_NUMBER, }, ) resp.raise_for_status() session = resp.json() # 2. Transfer to Maven's payment line response = VoiceResponse() response.say("Transferring you to our secure payment line.") dial = response.dial( caller_id=caller, action=f"{API_BASE}/payment-complete?caller={caller}", timeout=30, ) dial.number(session["phone_number"]) return Response(content=str(response), media_type="text/xml") @app.post("/payment-complete") async def payment_complete(caller: str): """Called after the Maven call ends and the caller returns.""" async with httpx.AsyncClient() as client: resp = await client.get( "https://api.trymaven.com/v1/sessions", headers={"Authorization": f"Bearer {MAVEN_API_KEY}"}, params={"caller": caller}, ) session = resp.json() response = VoiceResponse() if session["status"] == "payment-success": response.say("Your payment was successful. Thank you for calling!") elif session["status"] in ("payment-failed", "expired"): response.say("The payment didn't go through. Please call back to try again.") else: response.say("Thank you for calling. Goodbye.") response.hangup() return Response(content=str(response), media_type="text/xml") ``` ## Caller ID Verification If sessions are created but calls aren't connecting, verify the session exists for the right number: ```bash theme={"dark"} curl "https://api.trymaven.com/v1/sessions?caller=%2B14155551234" \ -H "Authorization: Bearer YOUR_API_KEY" ``` If this returns the session but calls still don't connect, the `callerId` on your `` doesn't match the `caller` you passed to session creation. ## Next Get notified when sessions complete. Explore the full API. Test with test cards and test mode keys. Integrating via a voice agent platform instead? # VAPI Source: https://docs.trymaven.com/integrations/vapi Integrate Maven voice payments with your VAPI agent # VAPI Integration Add PCI-compliant voice payments to your VAPI agent. When your agent needs to collect a payment, it calls Maven to create a session and transfers the caller to Maven's secure payment line. ## How It Works During a call, your VAPI agent calls the `collect_payment` function with the amount and caller's phone number. Maven creates a payment session and returns a phone number to transfer the caller to. Your agent uses the `transfer_to_payment` tool to transfer the caller to Maven's secure payment line. **Use your own phone number, not a VAPI phone number.** VAPI originates transfers from its own number — not the caller's. Maven matches sessions by caller ID, so the transfer must come from the customer's number. Configure your VAPI agent with your own phone number (e.g. Twilio, Vonage, or Telnyx) to preserve the caller ID. Maven collects the card details, processes the payment, and sends a [webhook](/integrations/webhooks) with the result. The caller is optionally transferred back to your agent via the `callback` number. ## Setup ### Automatic (Dashboard) The easiest way to set up is from the Maven Dashboard: In the [Maven Dashboard](https://app.trymaven.com), navigate to your app and click **Integrations**. Enter your VAPI Private Key and Maven API key. Optionally provide a VAPI Assistant ID to auto-attach the tools. Maven creates the following tools in your VAPI account: * `collect_payment` — creates a payment session * `transfer_to_payment` — transfers the caller to Maven's payment line * `get_session` — looks up a session by phone number * `cancel_session` — cancels a pending session Your VAPI Private Key is used once to create the tools and is never stored. ### Manual (Webhook URL) If you prefer to configure the tools yourself, use a webhook URL: ``` https://api.trymaven.com/integrations/vapi/webhook?token=YOUR_TOKEN ``` Generate a webhook token from the dashboard or via the API. The token encodes your API key, project, gateway, and mode so they don't need to be passed as query parameters. VAPI sends payloads in multiple formats (tool-calls, function-call, and API request). Maven auto-detects the format and handles all three. # Webhooks (Notifications) Source: https://docs.trymaven.com/integrations/webhooks Receive real-time notifications when payment sessions complete # Webhooks Maven sends an HTTP POST to your webhook URL when a payment session completes or fails. This is the recommended way to get payment results back into your system. **Voice and chat widget both use the same webhook.** One webhook URL per project, one handler in your code — it processes both channels. Use the `caller` field to distinguish: it's the customer's phone number for voice, and `null` for chat. ## When Webhooks Fire Webhooks are sent when a session reaches a terminal payment status: | Status | When | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `payment-success` | Charge was authorized **and captured** immediately (default) | | `payment-authorized` | Authorize.net auth-only charge succeeded — funds held but **not** captured. You settle the auth yourself in your gateway dashboard or via the gateway's API. | | `payment-failed` | Charge declined or gateway error | Webhooks are **not** sent for `expired`, `cancelled`, or `abandoned` sessions — poll the [GET session endpoint](/api-reference/overview) for those. ## Configuring Your Webhook URL Set a webhook URL per app in the [Maven Dashboard](https://app.trymaven.com): 1. Navigate to your app 2. Go to the **Settings** tab 3. Enter your webhook URL (must be HTTPS in production) 4. Save ## Verifying Webhook Signatures Your webhook URL is public, so anyone could POST a forged event to it. Maven signs every webhook with an HMAC so you can confirm it genuinely came from us and wasn't altered in transit. Verification is **optional but strongly recommended** for production. Signing is **backward compatible** — it only adds a header. If you don't verify it, your existing handler keeps working unchanged. Adopt verification whenever you're ready. ### The signing secret Each app has its own signing secret (format `whsec_…`). Find it in the [Dashboard](https://app.trymaven.com) under **App → Settings → Webhook → Signing secret**, where you can reveal, copy, and **rotate** it. Rotating generates a new secret and **invalidates the old one immediately**. Update your server with the new value before (or right after) rotating, or signatures will start failing. ### The `Maven-Signature` header Every webhook request includes: ``` Maven-Signature: t=1718500000,v1=4f3a9c...e1 ``` | Part | Meaning | | ---- | -------------------------------------------------------- | | `t` | Unix timestamp (seconds) when we signed the request | | `v1` | `HMAC_SHA256(secret, "{t}.{raw_body}")` as lowercase hex | The timestamp is part of the signed content, so it can't be altered without breaking the signature — this is what protects you against replay attacks. ### How to verify 1. Read the **raw request body** — the exact bytes, before any JSON parsing/re-serialization. 2. Parse `t` and `v1` from the `Maven-Signature` header. 3. Compute `HMAC_SHA256(secret, "{t}." + raw_body)`. 4. Constant-time compare it against `v1`. 5. Reject if `t` is older than your tolerance (e.g. 5 minutes) to block replays. ```python theme={"dark"} import hmac, hashlib, time, json from fastapi import FastAPI, Request, HTTPException WEBHOOK_SECRET = "whsec_..." # from the dashboard TOLERANCE_SECONDS = 300 def verify(raw_body: bytes, signature_header: str) -> bool: try: parts = dict(p.split("=", 1) for p in signature_header.split(",")) t, sig = parts["t"], parts["v1"] except (ValueError, KeyError): return False if abs(time.time() - int(t)) > TOLERANCE_SECONDS: return False # too old — possible replay signed = f"{t}.".encode() + raw_body expected = hmac.new(WEBHOOK_SECRET.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, sig) app = FastAPI() @app.post("/webhooks/maven") async def maven_webhook(request: Request): raw = await request.body() # RAW bytes, not request.json() if not verify(raw, request.headers.get("Maven-Signature", "")): raise HTTPException(status_code=401, detail="invalid signature") payload = json.loads(raw) # ... trusted return {"status": "ok"} ``` ```js theme={"dark"} const crypto = require("crypto"); const WEBHOOK_SECRET = process.env.MAVEN_WEBHOOK_SECRET; const TOLERANCE_SECONDS = 300; function verify(rawBody, signatureHeader) { const parts = Object.fromEntries( (signatureHeader || "").split(",").map((p) => p.split("=")), ); const { t, v1 } = parts; if (!t || !v1) return false; if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCE_SECONDS) return false; const expected = crypto .createHmac("sha256", WEBHOOK_SECRET) .update(`${t}.`) .update(rawBody) // Buffer of the raw body .digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)); } // Capture the raw body so the bytes match what we signed: app.post( "/webhooks/maven", express.raw({ type: "application/json" }), (req, res) => { if (!verify(req.body, req.get("Maven-Signature"))) { return res.status(401).send("invalid signature"); } const payload = JSON.parse(req.body.toString()); res.sendStatus(200); }, ); ``` **Sign the raw bytes, not the parsed JSON.** `{"a":1,"b":2}` and `{"b":2,"a":1}` are equal objects but different bytes, so re-serializing before hashing produces a mismatching signature. Always HMAC the request body exactly as received, and use a constant-time comparison (`hmac.compare_digest` / `crypto.timingSafeEqual`). Use the **Test** button next to your webhook URL in the dashboard — it sends a signed sample payload so you can validate your verification code end-to-end. ## Payload Format All webhook payloads share the same top-level fields. The `processor` object varies by gateway and mode — see [Processor Fields by Gateway](#processor-fields-by-gateway) for the full specs. ### Voice vs Chat The payload is **almost identical for voice and chat** — only one field differs: * **`caller`** is the customer's phone number for voice sessions, and `null` for chat sessions (no phone involved) Everything else — `status`, `processor`, `card_brand`, `card_last4`, error codes — is the same. A single webhook handler works for both. ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "charge", "gateway": "stripe", "caller": null, "card_brand": "visa", "card_last4": "4242", "processor": { "payment_intent_id": "pi_xxx", "charge_id": "ch_xxx", "card_brand": "visa", "card_last4": "4242" } } ``` ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "charge", "gateway": "stripe", "caller": "+14155551234", "card_brand": "visa", "card_last4": "4242", "processor": { "payment_intent_id": "pi_xxx", "charge_id": "ch_xxx", "card_brand": "visa", "card_last4": "4242" } } ``` ## Full examples by gateway ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "charge", "gateway": "stripe", "caller": "+14155551234", "card_brand": "visa", "card_last4": "4242", "processor": { "payment_intent_id": "pi_xxx", "charge_id": "ch_xxx", "receipt_url": "https://pay.stripe.com/receipts/...", "payment_method_id": "pm_xxx", "card_brand": "visa", "card_last4": "4242" } } ``` ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "tokenize", "gateway": "stripe", "caller": "+14155551234", "card_brand": "visa", "card_last4": "4242", "processor": { "payment_method_id": "pm_xxx", "cloned_payment_method_id": "pm_yyy", "cloned_customer_id": "cus_xxx", "card_brand": "visa", "card_last4": "4242" } } ``` ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "charge", "gateway": "authorizenet", "caller": "+14155551234", "card_brand": "visa", "card_last4": "4242", "processor": { "transaction_id": "80053776892", "auth_code": "D2U7TY", "response_code": "1", "avs_result_code": "Y", "cvv_result_code": "M", "cavv_result_code": "2", "network_trans_id": "V1NZI0NLBXIKFMK2RAC1YCV", "auth_only": false, "card_brand": "visa", "card_last4": "4242" } } ``` ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-authorized", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "charge", "gateway": "authorizenet", "caller": "+14155551234", "card_brand": "visa", "card_last4": "4242", "processor": { "transaction_id": "80053776894", "auth_code": "P60ZS3", "response_code": "1", "avs_result_code": "Y", "cvv_result_code": "M", "cavv_result_code": "2", "network_trans_id": "DTUM1U4E6VT99F6A5KZQ0SS", "auth_only": true, "card_brand": "visa", "card_last4": "4242" } } ``` Funds are held but not captured. Use `processor.transaction_id` to settle the charge later in your Authorize.net dashboard or via your own `priorAuthCaptureTransaction` API call. See [Authorize.net Setup → Capture Mode](/integrations/authorizenet-setup#capture-mode). ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "tokenize", "gateway": "authorizenet", "caller": "+14155551234", "card_brand": "visa", "card_last4": "4242", "processor": { "customer_profile_id": "123456789", "payment_profile_id": "987654321", "card_brand": "visa", "card_last4": "4242" } } ``` ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "charge", "gateway": "braintree", "caller": "+14155551234", "card_brand": "visa", "card_last4": "4242", "processor": { "transaction_id": "abc123", "braintree_status": "submitted_for_settlement", "customer_id": "cust_xxx", "payment_method_token": "token_xxx", "card_brand": "visa", "card_last4": "4242" } } ``` ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "tokenize", "gateway": "braintree", "caller": "+14155551234", "card_brand": "visa", "card_last4": "4242", "processor": { "customer_id": "cust_xxx", "payment_method_token": "token_xxx", "card_brand": "visa", "card_last4": "4242" } } ``` ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "charge", "gateway": "shift4", "caller": "+14155551234", "card_brand": "visa", "card_last4": "4242", "processor": { "charge_id": "char_xxx", "customer_id": "cust_xxx", "card_id": "card_xxx", "card_brand": "visa", "card_last4": "4242" } } ``` ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "tokenize", "gateway": "shift4", "caller": "+14155551234", "card_brand": "visa", "card_last4": "4242", "processor": { "customer_id": "cust_xxx", "card_id": "card_xxx", "card_brand": "visa", "card_last4": "4242" } } ``` ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "charge", "gateway": "fiserv", "caller": "+14155551234", "card_brand": "VISA", "card_last4": "4977", "processor": { "fiserv_transaction_id": "84653901038", "fiserv_order_id": "R-ddf385f2-...", "fiserv_payment_token": "A60759FC-B2E7-40E2-BC78-3C230C5AB7CB", "fiserv_state": "CAPTURED", "fiserv_status": "APPROVED", "approval_code": "279391", "response_code": "00", "response_message": "Function performed error-free", "card_brand": "VISA", "card_last4": "4977" } } ``` ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "tokenize", "gateway": "fiserv", "caller": "+14155551234", "card_brand": "VISA", "card_last4": "4977", "processor": { "fiserv_payment_token": "A60759FC-B2E7-40E2-BC78-3C230C5AB7CB", "ipg_transaction_id": "84653901037", "card_brand": "VISA", "card_last4": "4977" } } ``` ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "charge", "gateway": "jpmorgan", "caller": "+14155551234", "card_brand": "visa", "card_last4": "0026", "processor": { "jpmorgan_transaction_id": "397cc44c-fe58-4ab5-b880-ce5915ea6c59", "jpmorgan_state": "CLOSED", "jpmorgan_status": "SUCCESS", "approval_code": "tst696", "response_code": "APPROVED", "response_message": "Transaction approved by Issuer", "card_brand": "visa", "card_last4": "0026" } } ``` ```json theme={"dark"} { "session_id": "a1b2c3d4-...", "status": "payment-success", "project": "my-store", "environment": "live", "amount": 49.99, "currency": "USD", "mode": "charge", "gateway": "webhook", "caller": "+14155551234", "card_brand": "visa", "card_last4": "1111", "processor": { "webhook_transaction_id": "qp_12345", "card_brand": "visa", "card_last4": "1111" } } ``` ### Field Reference | Field | Type | Description | | ------------- | -------------- | ------------------------------------------------------------------------------------------------- | | `session_id` | string | Session UUID | | `status` | string | `"payment-success"`, `"payment-authorized"`, or `"payment-failed"` | | `project` | string | App slug | | `environment` | string | `"test"` or `"live"` | | `amount` | number | Amount in **dollars** (e.g., `49.99`) | | `currency` | string | Currency code (e.g., `"USD"`) | | `mode` | string | `"charge"` or `"tokenize"` | | `gateway` | string | `"stripe"`, `"authorizenet"`, `"braintree"`, `"shift4"`, `"fiserv"`, `"jpmorgan"`, or `"webhook"` | | `caller` | string \| null | Caller phone number in E.164 format (voice only; `null` for chat) | | `card_brand` | string \| null | Card brand (visa, mastercard, amex, etc.) | | `card_last4` | string \| null | Last 4 digits of the card | | `processor` | object \| null | Gateway-specific response fields (see below) | | `error` | object \| null | Present only when `status` is `"payment-failed"` | ### Error Object (failures only) ```json theme={"dark"} { "error": { "code": "card_declined", "message": "Your card was declined." } } ``` ### Processor Fields by Gateway The `processor` object contains different fields depending on the gateway and mode (`charge` vs `tokenize`). **Charge mode:** ```json theme={"dark"} { "payment_intent_id": "pi_xxx", "charge_id": "ch_xxx", "receipt_url": "https://pay.stripe.com/receipts/...", "payment_method_id": "pm_xxx", "card_brand": "visa", "card_last4": "4242", "postal_code": "90210", "exp_month": 12, "exp_year": 2027 } ``` | Field | Description | | ------------------- | -------------------------------------- | | `payment_intent_id` | Stripe PaymentIntent ID | | `charge_id` | Stripe Charge ID | | `receipt_url` | Stripe-hosted receipt URL | | `payment_method_id` | Stripe PaymentMethod ID | | `card_brand` | Card brand (visa, mastercard, etc.) | | `card_last4` | Last 4 digits of the card | | `postal_code` | Billing ZIP/postal code (if collected) | | `exp_month` | Card expiration month (if collected) | | `exp_year` | Card expiration year (if collected) | **Tokenize mode:** ```json theme={"dark"} { "payment_method_id": "pm_xxx", "cloned_payment_method_id": "pm_yyy", "cloned_customer_id": "cus_xxx", "card_brand": "visa", "card_last4": "4242", "postal_code": "90210", "exp_month": 12, "exp_year": 2027 } ``` | Field | Description | | -------------------------- | ----------------------------------------------------------------------------------- | | `payment_method_id` | Original Stripe PaymentMethod ID (on Maven's platform account) | | `cloned_payment_method_id` | PaymentMethod cloned to your Stripe account (absent if same as `payment_method_id`) | | `cloned_customer_id` | Customer created on your Stripe account | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | | `postal_code` | Billing ZIP/postal code (if collected) | | `exp_month` | Card expiration month (if collected) | | `exp_year` | Card expiration year (if collected) | **Charge mode:** ```json theme={"dark"} { "transaction_id": "80053776892", "auth_code": "D2U7TY", "response_code": "1", "avs_result_code": "Y", "cvv_result_code": "M", "cavv_result_code": "2", "network_trans_id": "V1NZI0NLBXIKFMK2RAC1YCV", "auth_only": false, "card_brand": "visa", "card_last4": "4242", "postal_code": "90210", "exp_month": 12, "exp_year": 2027 } ``` | Field | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `transaction_id` | Authorize.net transaction ID. In Authorize Only mode, use this as the `refTransId` for `priorAuthCaptureTransaction`. | | `auth_code` | Authorization code from the issuing bank | | `response_code` | Authorize.net response code (`1`=Approved, `2`=Declined, `3`=Error, `4`=Held for review) | | `avs_result_code` | AVS (Address Verification System) match result. `Y` = address+zip match. | | `cvv_result_code` | CVV match result. `M` = match. | | `cavv_result_code` | CAVV (3D Secure) result. `2` = passed. | | `network_trans_id` | Card network transaction ID (Visa/Mastercard) — needed for card-on-file flows and certain refunds | | `auth_only` | `true` when the project is set to **Authorize Only** capture mode (auth without capture). See [Capture Mode](/integrations/authorizenet-setup#capture-mode). | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | | `postal_code` | Billing ZIP/postal code (if collected) | | `exp_month` | Card expiration month (if collected) | | `exp_year` | Card expiration year (if collected) | **Tokenize mode:** ```json theme={"dark"} { "customer_profile_id": "123456789", "payment_profile_id": "987654321", "card_brand": "visa", "card_last4": "4242", "postal_code": "90210", "exp_month": 12, "exp_year": 2027 } ``` | Field | Description | | --------------------- | -------------------------------------- | | `customer_profile_id` | CIM Customer Profile ID | | `payment_profile_id` | CIM Payment Profile ID | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | | `postal_code` | Billing ZIP/postal code (if collected) | | `exp_month` | Card expiration month (if collected) | | `exp_year` | Card expiration year (if collected) | **Charge mode:** ```json theme={"dark"} { "transaction_id": "abc123", "braintree_status": "submitted_for_settlement", "customer_id": "cust_xxx", "payment_method_token": "token_xxx", "card_brand": "visa", "card_last4": "4242", "postal_code": "90210", "exp_month": 12, "exp_year": 2027 } ``` | Field | Description | | ---------------------- | ----------------------------------------------------- | | `transaction_id` | Braintree transaction ID | | `braintree_status` | Transaction status (e.g., `submitted_for_settlement`) | | `customer_id` | Braintree customer ID | | `payment_method_token` | Braintree payment method token | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | | `postal_code` | Billing ZIP/postal code (if collected) | | `exp_month` | Card expiration month (if collected) | | `exp_year` | Card expiration year (if collected) | **Tokenize mode:** ```json theme={"dark"} { "customer_id": "cust_xxx", "payment_method_token": "token_xxx", "card_brand": "visa", "card_last4": "4242", "postal_code": "90210", "exp_month": 12, "exp_year": 2027 } ``` | Field | Description | | ---------------------- | -------------------------------------- | | `customer_id` | Braintree customer ID | | `payment_method_token` | Braintree payment method token | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | | `postal_code` | Billing ZIP/postal code (if collected) | | `exp_month` | Card expiration month (if collected) | | `exp_year` | Card expiration year (if collected) | **Charge mode:** ```json theme={"dark"} { "charge_id": "char_xxx", "customer_id": "cust_xxx", "card_id": "card_xxx", "card_brand": "visa", "card_last4": "4242", "postal_code": "90210", "exp_month": 12, "exp_year": 2027 } ``` | Field | Description | | ------------- | -------------------------------------- | | `charge_id` | Shift4 charge ID | | `customer_id` | Shift4 customer ID | | `card_id` | Shift4 card ID | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | | `postal_code` | Billing ZIP/postal code (if collected) | | `exp_month` | Card expiration month (if collected) | | `exp_year` | Card expiration year (if collected) | **Tokenize mode:** ```json theme={"dark"} { "customer_id": "cust_xxx", "card_id": "card_xxx", "card_brand": "visa", "card_last4": "4242", "postal_code": "90210", "exp_month": 12, "exp_year": 2027 } ``` | Field | Description | | ------------- | -------------------------------------- | | `customer_id` | Shift4 customer ID | | `card_id` | Shift4 card ID | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | | `postal_code` | Billing ZIP/postal code (if collected) | | `exp_month` | Card expiration month (if collected) | | `exp_year` | Card expiration year (if collected) | **Charge mode:** ```json theme={"dark"} { "fiserv_transaction_id": "84653901038", "fiserv_order_id": "R-ddf385f2-...", "fiserv_payment_token": "A60759FC-B2E7-40E2-BC78-3C230C5AB7CB", "fiserv_state": "CAPTURED", "fiserv_status": "APPROVED", "approval_code": "279391", "response_code": "00", "response_message": "Function performed error-free", "card_brand": "VISA", "card_last4": "4977", "postal_code": "90210" } ``` | Field | Description | | ----------------------- | ----------------------------------------------------------------------- | | `fiserv_transaction_id` | Fiserv `ipgTransactionId` | | `fiserv_order_id` | Fiserv `orderId` | | `fiserv_payment_token` | Reusable Fiserv payment token (also returned on each charge for re-use) | | `fiserv_state` | `"CAPTURED"` | | `fiserv_status` | `"APPROVED"` on success | | `approval_code` | Fiserv authorization code | | `response_code` | Processor response code (`"00"` = success) | | `response_message` | Processor response message | | `card_brand` | Card brand (e.g. `VISA`) | | `card_last4` | Last 4 digits | | `postal_code` | Billing ZIP/postal code (if collected) | **Tokenize mode:** ```json theme={"dark"} { "fiserv_payment_token": "A60759FC-B2E7-40E2-BC78-3C230C5AB7CB", "ipg_transaction_id": "84653901037", "card_brand": "VISA", "card_last4": "4977", "postal_code": "90210" } ``` | Field | Description | | ---------------------- | ------------------------------------------------------- | | `fiserv_payment_token` | Reusable Fiserv payment token (UUID) | | `ipg_transaction_id` | Fiserv `ipgTransactionId` from the tokenization request | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | | `postal_code` | Billing ZIP/postal code (if collected) | **Charge mode:** ```json theme={"dark"} { "jpmorgan_transaction_id": "397cc44c-fe58-4ab5-b880-ce5915ea6c59", "jpmorgan_state": "CLOSED", "jpmorgan_status": "SUCCESS", "approval_code": "tst696", "response_code": "APPROVED", "response_message": "Transaction approved by Issuer", "card_brand": "visa", "card_last4": "0026", "postal_code": "90210" } ``` | Field | Description | | ------------------------- | ---------------------------------------------- | | `jpmorgan_transaction_id` | J.P. Morgan `transactionId` | | `jpmorgan_state` | `"CLOSED"` (captured) or `"OPEN"` (authorized) | | `jpmorgan_status` | `"SUCCESS"` on approval | | `approval_code` | Issuer approval code | | `response_code` | `"APPROVED"` on success | | `response_message` | Processor response message | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | | `postal_code` | Billing ZIP/postal code (if collected) | **Tokenize mode:** ```json theme={"dark"} { "jpmorgan_consumer_profile_id": "profile-789", "jpmorgan_payment_method_id": "pm-321", "jpmorgan_payment_token": "4012000845034026", "jpmorgan_transaction_id": "397cc44c-fe58-4ab5-b880-ce5915ea6c59", "card_brand": "visa", "card_last4": "0026", "postal_code": "90210" } ``` | Field | Description | | ------------------------------ | --------------------------------------------------------------------- | | `jpmorgan_consumer_profile_id` | Consumer profile ID (when profile creation is enabled) | | `jpmorgan_payment_method_id` | Payment method ID within the consumer profile | | `jpmorgan_payment_token` | Safetech network token — reusable as `accountNumber` in later charges | | `jpmorgan_transaction_id` | Verification `transactionId` | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | | `postal_code` | Billing ZIP/postal code (if collected) | With the `webhook` gateway, the processor fields are whatever **your endpoint** returned when Maven forwarded the card — see [Card Forwarding](/integrations/card-forwarding). **Charge mode:** ```json theme={"dark"} { "webhook_transaction_id": "qp_12345", "card_brand": "visa", "card_last4": "1111", "postal_code": "90210" } ``` **Tokenize mode:** ```json theme={"dark"} { "webhook_token": "tok_abc", "card_brand": "visa", "card_last4": "1111", "postal_code": "90210" } ``` | Field | Description | | ------------------------ | ------------------------------------------------------------- | | `webhook_transaction_id` | The `transaction_id` your forwarding endpoint returned | | `webhook_token` | The `token` your forwarding endpoint returned (tokenize mode) | | `card_brand` | Card brand | | `card_last4` | Last 4 digits | | `postal_code` | Billing ZIP/postal code (if collected) | ## Handling Webhooks Your webhook endpoint should: 1. Return a `200` status code quickly (within 5 seconds) 2. Process the payload asynchronously if needed 3. Be idempotent — use `session_id` to deduplicate ```python theme={"dark"} from fastapi import FastAPI, Request app = FastAPI() @app.post("/webhooks/maven") async def maven_webhook(request: Request): payload = await request.json() session_id = payload["session_id"] status = payload["status"] if status == "payment-success": card_last4 = payload.get("card_last4") caller = payload.get("caller") # phone for voice, None for chat processor = payload.get("processor", {}) # Update your order, send receipt, etc. await handle_payment_success(session_id, processor) elif status == "payment-failed": error = payload.get("error", {}) await handle_payment_failure(session_id, error) return {"status": "ok"} ``` ## Recovering a missed webhook / checking status Webhook delivery can fail (endpoint down, network blip). To reconcile, call the **authenticated** session API with your API key. It returns the current `status` and `processor` details for **both voice and chat** sessions — including terminal states like `payment-success` — so it's the source of truth after a missed webhook: ``` GET /v1/sessions/{session_id} Authorization: Bearer ``` Do **not** use `GET /v1/widget-sessions/{session_id}` for reconciliation. That is the chat **iframe's** internal, unauthenticated endpoint — for security it returns **`410 Gone` with no data** once a payment succeeds (so completed transactions can't be scraped by guessing IDs). That's why its success response doesn't match the webhook. Use the authenticated `GET /v1/sessions/{session_id}` above. The `processor` object here uses **gateway-namespaced** field names (e.g. `processor.shift4_charge_id`, `processor.authnet_transaction_id`), whereas the webhook payload uses raw keys (e.g. `processor.transaction_id`). Same values, different names — map accordingly. ## Best Practices Return a `200` response immediately and process the webhook asynchronously. Maven uses a 5-second delivery timeout; if your endpoint is slower, the request is treated as failed and retried. Use the `session_id` to deduplicate — check if you've already processed this session before acting on it. Confirm the event came from Maven by verifying the `Maven-Signature` header — see [Verifying Webhook Signatures](#verifying-webhook-signatures). As an additional check, you can also confirm the `session_id` belongs to your organization via the API: ``` GET /v1/sessions/{session_id} ``` If your endpoint returns a 5xx or times out, Maven retries up to 3 times with exponential backoff (1s, 2s, 4s). A 4xx response is treated as a rejection and is **not** retried. If all attempts fail the webhook is dropped, so for critical flows also poll the session status as a fallback. # Introduction Source: https://docs.trymaven.com/introduction Collect credit card payments securely — over voice or inside your chatbot — with Maven Maven is payment infrastructure for AI applications. Collect credit card payments securely over voice calls **or** inside a chatbot, using any major payment gateway. Collect card details via natural speech over phone calls. Embed a secure card form inside your chatbot or web app. Stripe, Authorize.net, Braintree, Shift4, or Fiserv — same API. ## Two ways to collect Your server calls the Maven API with the customer's phone number, amount, and payment mode. Your voice AI agent transfers the caller to Maven's secure payment line. Maven's voice agent guides the caller through reading their card number, expiry, and CVV. Card details are tokenized through VGS and the payment is processed through your gateway. Receive a webhook with the payment status and gateway details. Your server calls `POST /v1/widget-sessions` with the amount and gateway. `Maven.createPayment({ sessionId }).mount("#slot")` — the card form appears inline. Card data goes browser → Maven iframe → VGS → gateway. Your page never touches it. `onSuccess({ transaction_id, card_brand, card_last4 })` on your page, plus a webhook to your server. ## Supported Gateways | Gateway | Charge | Tokenize | Voice | Chat | | ----------------- | ------ | -------- | ----- | ---- | | **Stripe** | ✓ | ✓ | ✓ | ✓ | | **Authorize.net** | ✓ | ✓ | ✓ | ✓ | | **Braintree** | ✓ | ✓ | ✓ | ✓ | | **Shift4** | ✓ | ✓ | ✓ | ✓ | | **Fiserv** | ✓ | ✓ | ✓ | ✓ | ## Security Your server never touches card data in either flow — Maven handles tokenization and the gateway call. See [Chat Widget Overview](/widget/overview) or [Voice Overview](/quickstart) for the full data-flow diagram. ## Next Steps Collect your first voice payment. Embed the widget in your chatbot. Full endpoint docs with an interactive playground. Server-side payment notifications. # Voice Quickstart Source: https://docs.trymaven.com/quickstart Collect your first voice payment in 5 minutes Create an app, connect a gateway, and collect your first payment. ## 1. Set Up Your Dashboard Create an account at [app.trymaven.com](https://app.trymaven.com). Go to **Apps** and click **Create App**. Give it a name — Maven generates a slug (e.g., `my-store`) that you'll use in API calls. In your app, go to the **Payments** tab and connect Stripe, Authorize.net, or Braintree. See the setup guides for [Stripe](/integrations/stripe-connect), [Authorize.net](/integrations/authorizenet-setup), or [Braintree](/integrations/braintree-setup). Go to **Settings > API Keys** and create a test key. It starts with `mvn_test_`. Copy it immediately — you won't see it again. ## 2. Try the Playground Open the **Playground** from the sidebar. Select your app, gateway, and mode, enter a phone number and amount, and create a test session. You'll see the session status update in real time as the caller progresses through the payment flow. **Payment modes:** Use **Charge** to process payment immediately, or **Tokenize** to save the card for later without charging. ## 3. Get the Result Configure a [webhook](/integrations/webhooks) to get notified when sessions complete. Maven sends a POST to your app's webhook URL with the payment status, card brand, last 4 digits, and gateway-specific details. Set your webhook URL in the app's **Settings** tab. See the [Webhooks guide](/integrations/webhooks) for the full payload format. See the [API Reference](/api-reference/overview) for creating sessions programmatically. ## Session Lifecycle Sessions progress through these statuses: ``` created → collecting-card → collecting-expiry → collecting-cvv → processing → payment-success ``` | Status | Terminal | Description | | ------------------------ | -------- | -------------------------------- | | `created` | No | Awaiting caller transfer | | `collecting-card` | No | Collecting card number | | `collecting-expiry` | No | Collecting expiry date | | `collecting-cvv` | No | Collecting CVV | | `collecting-postal-code` | No | Collecting ZIP code | | `processing` | No | Processing payment | | `payment-success` | **Yes** | Payment completed | | `payment-failed` | **Yes** | Payment declined | | `expired` | **Yes** | Session timed out (5-minute TTL) | | `cancelled` | **Yes** | Cancelled via API | Sessions expire after **5 minutes** (PCI compliance requirement). Most voice sessions complete in 60-90 seconds. ## Test Cards Use these with test mode keys (`mvn_test_`): | Card | Number | CVV | Expiry | | ---------- | --------------------- | ------------ | --------------- | | Visa | `4242 4242 4242 4242` | Any 3 digits | Any future date | | Mastercard | `5555 5555 5555 4444` | Any 3 digits | Any future date | | Amex | `3782 822463 10005` | Any 4 digits | Any future date | ## Next Get notified when sessions complete. Explore the full API. # Voice Testing Source: https://docs.trymaven.com/testing Test your voice integration end-to-end without reading real card numbers # Testing Your Integration Maven gives you a few tools for testing your integration end-to-end without having to read out a real card number on every call. ## Test Mode Any session created with a `mvn_test_` API key runs in **test mode**: * All gateway charges go to the gateway's sandbox environment (Stripe test mode, Authorize.net sandbox, Braintree sandbox, Shift4 test mode, Fiserv sandbox) * No real money moves * Webhooks fire normally so you can test your downstream flow * Sessions are tagged with `environment: "test"` in the database and the API response Use test mode for development, CI, and any integration testing. ## The "mango" Magic Word Reading out a 16-digit card number plus expiry plus CVV is tedious when you're testing your downstream flow over and over. Maven includes a shortcut: **in test mode only**, when the caller says the word **"mango"** at any point during the call, Maven instantly completes the session as a successful payment without going through card collection or hitting the gateway. ### How it works 1. You create a test session as usual (via `POST /v1/sessions` with a `mvn_test_` API key) 2. You call into the session 3. At any point — during the greeting, while the bot is asking for the card number, expiry, CVV — you say **"mango"** 4. Maven immediately: * Marks the session as `payment-success` * Inserts a synthetic transaction record with a fake `transaction_id` like `test_skip_1775720123456` * Speaks the normal success TTS message * **Fires your webhook** with `status: "payment-success"` and the synthetic transaction details * **Runs the transfer-back** if the session has a `callback` configured * Hangs up the call That's it — your full integration runs end-to-end (TTS, transfer, webhook delivery, your own webhook handler) without any real card processing. ### Webhook payload The webhook you receive looks like a normal `payment-success` webhook, with `processor.test_skip: true` to indicate it was synthetic: ```json theme={"dark"} { "event": "payment.success", "session_id": "a1b2c3d4-...", "status": "payment-success", "amount": 4999, "currency": "USD", "mode": "charge", "gateway": "authorizenet", "caller": "+14155551234", "card_brand": "visa", "card_last4": "0000", "processor": { "transaction_id": "test_skip_1775720123456", "auth_code": "TESTOK", "test_skip": true, "card_brand": "visa", "card_last4": "0000" } } ``` ### What gets bypassed When you say "mango" in test mode, the following are **skipped entirely**: * VGS card tokenization * Gateway charge (Authorize.net, Stripe, Braintree, Shift4, Fiserv) * Card validation (Luhn, brand detection, expiry) * Retry logic * Field-by-field collection state machine What still runs normally: * Success TTS message * Post-payment transfer-back to your callback number/SIP URI * Webhook delivery * DB session + transaction records ### Production safety The mango shortcut is **gated on test mode**. If a real caller says "mango" during a production call, nothing happens — the word is treated as ordinary speech and the bot continues asking for the card number. There is no way to trigger the shortcut on a live API key. The detection uses a strict whole-word regex (`\bmango\b`), so partial matches don't trigger: | Caller says | Triggers? | | ----------------------- | ------------------------------------ | | "mango" | ✅ | | "Mango." | ✅ (case-insensitive, punctuation OK) | | "I want a mango please" | ✅ (whole word in a sentence) | | "mang" | ❌ (not a complete word) | | "mangoes" | ❌ | ## Sandbox Test Cards If you want to actually run a charge through the gateway sandbox (instead of using the mango shortcut), use the test card numbers documented by each gateway: | Gateway | Test card | | ------------- | --------------------------------------------------- | | Stripe | `4242 4242 4242 4242`, any future expiry, any CVV | | Authorize.net | `4111 1111 1111 1111`, any future expiry, any CVV | | Braintree | `4111 1111 1111 1111`, any future expiry, any CVV | | Shift4 | `4242 4242 4242 4242`, any future expiry, any CVV | | Fiserv | `4035 8740 0042 4977`, any future expiry, CVV `977` | Most gateways have additional cards that simulate specific decline reasons (insufficient funds, expired, etc.) — see the gateway's documentation for the full list. ### Authorize.net sandbox responses Maven routes test-mode Authorize.net charges to the real sandbox environment (`apitest.authorize.net`) and returns the **actual response data** from the sandbox processor — real `transaction_id`, `auth_code`, AVS/CVV/CAVV result codes, and `network_trans_id`. Earlier versions of Maven returned mocked values (`transaction_id: "0"`, `auth_code: "000000"`); this is no longer the case. This means you can fully test your Authorize.net integration in sandbox — including the [Authorize Only capture flow](/integrations/authorizenet-setup#capture-mode) — and the payload you receive in sandbox matches exactly what you'll see in production. ## Tips During development, point your webhook URL at a service like [webhook.site](https://webhook.site) so you can inspect the exact payload Maven sends without writing a server. You don't have to wait for any particular field — say "mango" during the greeting, in the middle of reading the card number, or during expiry collection. The shortcut fires immediately. If your session has a `callback` configured, mango still triggers the post-payment transfer. This is the easiest way to verify your full call flow including the bridge back to your agent. There is no way to enable the magic word in production. If you need to "test in production" (e.g., to verify a deploy), you need a real card. # Voice Settings Source: https://docs.trymaven.com/voice-settings Configure the voice your callers hear during payment collection # Voice Settings Configure the voice your callers hear during payment collection. ## Default Voice Every app uses Maven's default ElevenLabs voice at no extra cost. No setup required — your callers will hear a natural-sounding voice out of the box. ## Custom Voices To use your own voice, model, or provider: Go to **Settings > Voice** in the [Maven Dashboard](https://app.trymaven.com) and add your API key for one or more providers. Navigate to your app's **Voice** tab. Select a TTS provider, model, and voice ID. Your custom voice will be used for all future payment calls on that app. ### Supported Providers | Provider | Key format | Notes | | -------------- | ---------- | ---------------------------------------------------------------------- | | **ElevenLabs** | `sk_...` | High-quality voices with streaming. Default model: `eleven_flash_v2_5` | | **Deepgram** | `dg_...` | Aura TTS voices (e.g., `aura-2-thalia-en`) | | **Cartesia** | `sk-...` | Sonic TTS voices. Requires voice ID (UUID from Cartesia dashboard) | Without your own API key, voice customization is locked. You'll see the default voice settings and cannot change the model or voice ID. ## AI Agent Mode By default, Maven uses an AI agent (powered by GPT-4o-mini) to have natural conversations during card collection. The agent: * Greets the caller naturally * Acknowledges each field (e.g., "Got your Visa! Now please provide your expiration date.") * Handles errors conversationally (e.g., "That card number doesn't look right, let's try again.") * Confirms payment success If the agent times out or encounters an error, Maven falls back to static template messages automatically. You can disable AI agent mode per app by toggling **AI Conversation** off in your app's Voice tab. When disabled, Maven uses deterministic template messages for all interactions. ## Custom Greetings Customize the greeting message your callers hear when they connect to the payment line. Set greetings per app in the **Voice** tab. ### Greeting Types | Greeting | When used | | ----------------------------------- | ------------------------------------------------------ | | **Charge greeting** | Caller is in charge mode and can speak or use keypad | | **Charge greeting (keypad only)** | Caller is in charge mode, DTMF-only | | **Tokenize greeting** | Caller is in tokenize mode and can speak or use keypad | | **Tokenize greeting (keypad only)** | Caller is in tokenize mode, DTMF-only | ### Default Greetings If no custom greeting is set, Maven uses built-in defaults: * **Charge**: *"Hi! To process your payment of \$X.XX, go ahead and read your card number slowly, one group at a time, or enter it on your keypad."* * **Tokenize**: *"Hi! To save your card on file, go ahead and read your card number slowly, one group at a time, or enter it on your keypad."* ### Template Variables Use these placeholders in your custom greeting and Maven will replace them with the session values: | Variable | Replaced with | | ----------------- | ----------------------------------------------------- | | `{{amount}}` | Payment amount (e.g., `$49.99`) | | `{{description}}` | Charge description (if provided) | | `{{mode}}` | `"payment"` for charge, `"card on file"` for tokenize | ## Transfer Message Customize the message spoken before transferring the caller back to your agent. This only plays when a `callback` number is provided in the session. Set it per app in the **Voice** tab. Default: *"Please stay on the line while we connect you back."* ## Input Modes ### Voice + Keypad (default) Callers can speak their card details or enter them on their phone's keypad. Maven uses speech recognition (ASR) and DTMF detection simultaneously. Once the caller starts using one method, it locks to that mode for the rest of the call. ### Keypad Only (DTMF) Enable **DTMF Only** in your app settings to restrict input to keypad entry only. This disables speech recognition entirely. Useful for noisy environments or when you want a more predictable experience. ## Postal Code By default, Maven collects card number, expiry, and CVV. Enable **Require Postal Code** in your app settings to also collect the caller's ZIP/postal code for address verification (AVS). # Customization Source: https://docs.trymaven.com/widget/customization Match the widget to your brand from the dashboard Brand the widget to match your chat UI — colors, text, fields, sizing — all from the dashboard. No code. ## Customize in the dashboard Go to [app.trymaven.com](https://app.trymaven.com) → your app → **Chat Payments** tab → **Customize**. * **Theme:** mode (light/dark), button color, text color, border radius * **Sizing:** card width, padding, font size * **Labels:** header, pay button, deposit line, success/failure text * **Fields:** toggle cardholder name and billing ZIP The preview iframe on the right updates as you change things. Every widget session on this project automatically uses your saved settings. No code to change. ## What you can customize Light or dark mode, button color, text color, background, borders, border radius. Card max-width (narrow / default / wide / full), internal padding, font size. The outer iframe width is controlled by your chatbot's CSS — the inner card sizing is controlled here. Header title, pay button text, processing text, deposit line, success/failure titles. Show or hide the cardholder name and billing ZIP fields. Card number, expiry, and CVV are always shown. ## Running multiple brands? If you serve multiple downstream brands from one Maven account (e.g. a platform like VAPI or Retell reselling Maven to their customers), create one project per brand. Each project has its own saved theme, labels, and fields — just pass the right project slug in your `POST /v1/widget-sessions` call for each customer. # Chat Payment Widget Source: https://docs.trymaven.com/widget/overview Collect payments inline in your chatbot or web app in 10 lines of code The Maven chat payment widget lets your chatbot or web app collect card payments without redirecting the customer, sending them to Stripe Checkout, or handling card data yourself. Drop in a ` ``` This exposes a global `Maven` object with one method: `createPayment()`. When your chatbot receives the `session_id`, mount the widget into a `
` inside the chat message: ```javascript theme={"dark"} const payment = Maven.createPayment({ sessionId: "", onSuccess: (result) => { // result = { transaction_id, card_brand, card_last4, amount_cents, currency, status } // Typical things to do here (all optional — Maven already shows // a green "Payment confirmed" screen inside the iframe): chatbot.sendMessage(`Paid — ${result.card_brand} •• ${result.card_last4}`); await db.orders.markPaid(orderId, result.transaction_id); // or: window.location = `/thanks?txn=${result.transaction_id}`; }, onFailure: (error) => { // error = { error_code, error_message } chatbot.sendMessage(`Payment didn't go through: ${error.error_message}`); }, }); payment.mount("#chat-payment-slot"); ``` The iframe appears inline. Customer types their card, hits pay. Your callbacks fire — `onSuccess` and `onFailure` are **hooks into your app** so you can continue the chatbot conversation, update your database, or redirect the customer. The widget itself handles the card form, charging, and the success/failure UI inside the iframe. Configure a webhook URL on the project. Maven fires the same `payment-success` / `payment-failed` event for widget payments as it does for voice — with an additional `source: "chat"` field. See [Webhooks](/integrations/webhooks) for the full payload. ## Common errors `POST /v1/widget-sessions` can return these if something's off in your setup: | Status | Code | What it means | Fix | | ------ | ----------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `401` | — | Bad or missing API key | Check the `Authorization: Bearer mvn_test_...` header | | `404` | `project_not_found` | Project slug doesn't exist in your org | Double-check the `project` value matches a slug in your dashboard | | `422` | `gateway_not_connected` | Gateway isn't connected for this env | Connect the gateway in the dashboard (Gateways tab). Test keys need test credentials; live keys need live credentials. | | `422` | `invalid_amount` | `amount_cents` is 0 or negative in charge mode | Pass `amount_cents > 0`, or use `mode: "tokenize"` to save a card without charging | | `429` | — | Rate limited | Back off; defaults are 20 session creates/minute per IP | If the widget mounts but doesn't render, check your browser console. The usual culprit is `Maven is not defined` — that means the ` ``` ## Next Complete options, callbacks, and controller methods. Theme, labels, fields, and sizing options. # JavaScript SDK Reference Source: https://docs.trymaven.com/widget/sdk Complete API for Maven.createPayment() — options, callbacks, and controller methods The Maven JS SDK is a thin wrapper that mounts the secure iframe into your page and forwards lifecycle events to callbacks. It does **not** ship business logic — all the heavy lifting (tokenization, gateway charging) happens server-side. ## Installation ```html theme={"dark"} ``` After load, the global `Maven` object is available. The SDK is served with `Cache-Control: public, max-age=31536000, immutable` — the versioned URL (`/v1/widget.js`) never changes, so browsers cache it for a year. Bump the major version only for breaking changes. ## `Maven.createPayment(options)` Creates a widget controller. Does not mount until you call `.mount(target)`. ### Options | Option | Type | Required | Description | | ----------- | ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- | | `sessionId` | string | ✓ | Session ID returned by `POST /v1/widget-sessions` | | `baseUrl` | string | | Overrides Maven API base (defaults to wherever `widget.js` is loaded from). Only needed for multi-region or pointing at staging. | | `onReady` | `() => void` | | Fires when the iframe is fully loaded and the form is interactive. | | `onSuccess` | `(result) => void` | | Fires when payment succeeds. See result shape below. | | `onFailure` | `(error) => void` | | Fires when payment fails. See error shape below. | Theme, labels, and fields are set per-project in the dashboard — not on the SDK call. See [Customization](/widget/customization). ### Returns A controller object: ```typescript theme={"dark"} interface PaymentController { mount(target: string | HTMLElement): PaymentController; updateTheme(theme: Theme): PaymentController; updateLabels(labels: Labels): PaymentController; updateFields(fields: Fields): PaymentController; destroy(): void; iframe: HTMLIFrameElement; } ``` ## `controller.mount(target)` Inserts the iframe into the DOM. ```javascript theme={"dark"} controller.mount("#chat-payment-slot"); // CSS selector controller.mount(document.getElementById("slot")); // element directly ``` The iframe fills 100% of the target's width and auto-grows its height via `maven:resize` postMessage — your container controls the width, Maven handles the height. ## `onSuccess(result)` Fires when the gateway charge succeeds. ```javascript theme={"dark"} onSuccess: (result) => { result.transaction_id // gateway transaction ID result.card_brand // "visa", "mastercard", "amex", "discover" result.card_last4 // "4242" result.amount_cents // 10000 result.currency // "USD" result.status // "succeeded" } ``` ## `onFailure(error)` Fires when the charge is declined or errors. ```javascript theme={"dark"} onFailure: (error) => { error.error_code // "card_declined", "insufficient_funds", etc. error.error_message // Human-readable message to show the customer } ``` The widget itself shows the failure state (red "Payment Failed" box + "Try Again" button). Your `onFailure` callback is for doing things in *your* UI — sending a follow-up chat message, tracking the failure in analytics, etc. ## Cleanup When the chat bubble unmounts (user navigates away, chat closes, etc.), destroy the widget to remove listeners: ```javascript theme={"dark"} payment.destroy(); ``` ## Full example ```javascript theme={"dark"} const payment = Maven.createPayment({ sessionId: sessionIdFromServer, onReady: () => { console.log("widget loaded"); }, onSuccess: (result) => { chatbot.sendBotMessage( `Payment received — ${result.card_brand.toUpperCase()} ending ${result.card_last4}.` ); orderService.markPaid(orderId, result.transaction_id); }, onFailure: (error) => { chatbot.sendBotMessage( `Payment didn't go through: ${error.error_message}. Want to try another card?` ); }, }); payment.mount("#chat-payment-slot"); ``` ## Security model * The iframe is served from **Maven's domain**, not yours — your page's JavaScript cannot read the card inputs or inspect the form (enforced by the browser's same-origin policy). * Communication between your page and the iframe is **one-way via `postMessage`** — the iframe posts events up, the SDK listens. No DOM access. * The `sessionId` is a **single-use credential**. It expires after a short TTL (default 5 minutes) and can only be used to charge the specific amount on the specific project it was created for. ## Next Theme, labels, fields, sizing. Server-side confirmation events.