Skip to content

Agentic Commerce MCP Server

VantagePay exposes its Agentic Commerce Protocol (ACP) capabilities as a remote Model Context Protocol (MCP) server. Connect it to an AI assistant and the assistant can browse your merchant catalogue, build a basket, take the buyer's card details, and settle a real card payment - entirely inside the chat.

Practically, that means a buyer can type this into ChatGPT or Claude:

"Buy me GHS 20 of MTN airtime for 024 123 4567."

…and the assistant discovers the product, collects the mobile number, authorises the card, submits the payment, and reports the confirmation number back - without a checkout page, a redirect, or a hosted form.

A completed airtime purchase

At a glance

Sandbox endpointhttps://sandbox-api.vantagepay.dev/mcp
TransportMCP Streamable HTTP (stateless)
AuthenticationAuthorization: Bearer <access token> on every request
Tools exposedsearch_products, create_checkout, update_checkout, delegate_payment, complete_checkout, check_payment_status
Resources / promptsNone - this server is tools-only
CurrenciesAny ISO 4217 code your merchant account supports (for example GHS, ZAR, USD)
Amount unitMinor units only (cents / pesewas)

Why MCP and not the REST API?

The ACP REST API is for you - code you write that orchestrates a checkout. The MCP server is for an AI agent you do not control: the tool descriptions carry the sequencing rules, amount conventions and buyer-messaging guidance the model needs, so a general-purpose assistant gets the flow right without any prompt engineering on your side.

Prerequisites

  1. A VantagePay sandbox account. You can use the following bearer token to test the MCP server in the sandbox:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ6Z2EiLCJpc3MiOiJaR0EiLCJleHAiOjE4NzIyMzU3ODgsIm5iZiI6MTc4NTgzNTc4OCwicmVmZXJlbmNlIjoiNzExYzZjYTAtMjI0OC00NDdhLThhOTAtZjE2NjEyMjM1NDU0IiwidXNlcl9uYW1lIjoiVmFudGFnZVBheSBNQ1AiLCJlbWFpbF9hZGRyZXNzIjoiZGV2QHZhbnRhZ2VwYXkuY29tIiwiY2FuX2Rpc2FibGVfY29uZmlybWF0aW9uIjoidHJ1ZSIsInRva2VuX3JvbGUiOiJVc2VyIiwiYWNjZXNzX3BvbGljeSI6WyIvbWNwLFBPU1QiLCIvbWNwLEdFVCJdLCJqdGkiOiJkMGExYzI0Yi05NDJlLTQ2MWMtOTIyYy0yZTk3NjZhNzViMDkiLCJpYXQiOjE3ODU4MzU3ODgsInRva2VuX3R5cGUiOiJBY2Nlc3MifQ.A2SN_hZLm8_1IIXb5oXRm0L0_6cMKzjVrDQo0aXBX3AC7qpC6EhHufoAq50snaEzbClI0M694bSTXC3SlRN6KC74zgk-2pp3muXcsrVWiG0wN0etjFABSDeX6m0kuS4wEPICPXUGi3JAODzqyRSjz2GM4RJzPBLQPH88arsQ-Vd24BSeBHm9xi2gcQORto9RRkJDGBQNmmsKCDtZa9Z0uHxj8xJi5xau6sFB9NN2LUxExMHE3FV9QfoBm1wD59BQPosSybJjTYPjWRi0-9Y-egnppBa_FBH3eZtIc3OyhjVPTG8fENuYFnUq4OWEmUvhfmUc2LDJWqXzJz53242NwA

  1. An access token. Call POST /v1/auth/login and use the returned accessToken as the bearer token (see Authentication).
  2. Active products. search_products returns the active products on your merchant account. If the catalogue is empty every purchase attempt will stall at step 1.
  3. An MCP-capable client. ChatGPT (developer mode), Claude, Claude Code, Codex, or any client that speaks MCP Streamable HTTP.

Treat the token like a payment credential

Anyone holding the token can create checkout sessions and submit payments on your merchant account. Use a sandbox token for testing, never paste a production token into a shared workspace connector, and rotate it if it leaks.

The tools

Six tools, called in a fixed order. Everything an agent needs is in the tool descriptions, so you do not need to prime the model with instructions.

ToolPurposeTypical call count
search_productsDiscover products, prices and the buyer details each product needs1
create_checkoutOpen a session for a basket of line items1
update_checkoutReplace the basket - used to add missing product fields or change quantities0–n
delegate_paymentCapture the card and mint a single-use, allowance-bounded vault token1
complete_checkoutSubmit the payment and create the order1
check_payment_statusPoll until the payment reaches a terminal state2–10

search_products

Searches the merchant catalogue and returns a human-readable listing plus an internal data block the agent uses to build the basket.

ParameterTypeRequiredDescription
querystringNoFree-text filter matched against product name, description, category and merchant name. Omit to list everything.

Matching is word-based and case-insensitive: a product matches if any word in the query appears in one of those fields.

What comes back

text
[BUYER MESSAGE]
Here's what's available:

• MTN Airtime (variable price) - Top up any MTN Ghana number
  (will need: Mobile Number)
• ECG Prepaid Units (50.00 GHS) - Electricity credit

[INTERNAL AGENT DATA - never show to buyer; use for create_checkout calls]
• MTN Airtime: product_reference=c48f9aa1-b93f-4ab2-a5d8-91857aab2d12 | REQUIRED extra_fields: mobileNumber (ask: "Mobile Number", matches ^0[2-5][0-9]{8}$)
• ECG Prepaid Units: product_reference=7b2e0f31-59aa-4d0c-8f0a-2b7c1d9e4a55 | REQUIRED extra_fields: meterNumber (ask: "Meter Number", free text)

The internal block gives the agent the exact product_reference to pass, the exact extra_fields key names to use, and a validation hint per field (a fixed list of allowed values, a regex, or free text).

create_checkout

Opens a session and prices the basket.

ParameterTypeRequiredDescription
currencystringYesISO 4217 code for the whole order. Every line item must be in this currency.
lineItemsJsonstringYesJSON array of line items (see below).

Each line item object accepts:

FieldTypeRequiredDescription
product_referencestringYesThe product ID from search_products.
namestringYesDisplay name used on the receipt.
unit_priceintegerYesPrice per unit in minor units (cents / pesewas).
quantityintegerYesNumber of units, minimum 1.
extra_fieldsobjectConditionalBuyer-supplied values, keyed by the field key from search_products. Required when the product has is_required fields.
description, image_url, merchant_reference, currencystringNoOptional per-line overrides.
json
[
  {
    "product_reference": "c48f9aa1-b93f-4ab2-a5d8-91857aab2d12",
    "name": "MTN Airtime",
    "unit_price": 2000,
    "quantity": 1,
    "extra_fields": { "mobileNumber": "0241234567" }
  }
]

The response returns the session ID, the order total in minor units, the session expiry, and - if any required product field is still missing - a ready-to-paste lineItemsJson template with <Field Name> placeholders for the agent to fill in via update_checkout.

update_checkout

Replaces the entire basket for a session. Used when the buyer changes their mind, and whenever create_checkout reported missing product fields.

ParameterTypeRequiredDescription
sessionIdstringYesSession ID from create_checkout.
lineItemsJsonstringYesThe complete replacement basket, same shape as create_checkout.

This is a replace, not a merge

Omitted line items are dropped, and omitted extra_fields keys are lost. The tool response always hands the agent a full template containing the current prices and the values already collected - agents should copy that template verbatim and fill in only the placeholders, rather than re-deriving prices from the conversation.

delegate_payment

Takes the raw card details, validates the spending allowance, and stores an encrypted, single-use vault token (vt_…) that is scoped to one session, one merchant, one currency and one maximum amount.

ParameterTypeRequiredDescription
cardNumberstringYes16–19 digit PAN.
expiryMonthintegerYes1–12.
expiryYearintegerYes4-digit year.
checkoutSessionIdstringYesSession ID from create_checkout.
merchantIdstringYesMerchant reference the authorisation is bound to.
maxAmountInCentsintegerYesAuthorisation ceiling in minor units. Must be ≥ the session total.
currencystringYesMust match the session currency.
cvcstringNo3–4 digit card security code.
cardholderNamestringNoName as printed on the card.

Guarantees enforced server-side:

  • The token is single use - it is deleted the moment complete_checkout redeems it, so it cannot be replayed.
  • The token expires after 15 minutes (and never outlives the checkout session).
  • The stored card blob is encrypted at rest and never returned to the agent.
  • Completion is rejected if the session total exceeds maxAmountInCents.
  • 3-D Secure step-up and buyer confirmation are disabled for agentic payments, which is what allows the purchase to finish inside the conversation.

Where does merchantId come from?

It is the merchant's reference GUID. The ACP product feed returns it as merchant_reference on every product (GET /v1/acp/products). Most integrations already know their own merchant reference and hard-code it in the agent's system prompt or connector description; if yours does not, read it from the product feed once and cache it.

complete_checkout

Redeems the vault token, builds the payment request, and queues it for processing.

ParameterTypeRequiredDescription
sessionIdstringYesSession being finalised.
vaultTokenstringYesThe vt_… token from delegate_payment.
buyerFirstNamestringNoUsed to personalise the confirmation.
buyerEmailstringNoWhere the order confirmation is sent.

This returns before the money moves

complete_checkout returns as soon as the payment is queued - not when it settles. The session moves to complete_in_progress, and the agent must poll check_payment_status. The tool description instructs the model to say "your payment is being processed" rather than "payment successful" at this point.

check_payment_status

Polls the underlying transaction batch and collapses it into one of three agent-visible outcomes.

ParameterTypeRequiredDescription
sessionIdstringYesSession being polled.
Reported statusSession statusWhat the agent does
processingcomplete_in_progressWait 3–5 seconds and poll again.
completedcompletedStop polling and present the confirmation number, amount and timestamp.
failedrequires_escalationStop polling and offer the buyer a different card.

Sandbox payments typically settle in a handful of seconds, so expect two to four polls. The tool description tells the agent to keep the buyer informed rather than give up silently if it takes longer.

How tool responses are structured

Every tool returns a single string split into two labelled sections:

text
[BUYER MESSAGE]
To complete the payment, please share your card details - card number,
expiry date (month and year), and the CVC from the back of the card.

[INTERNAL AGENT DATA - never show to buyer]
session_id=9f1c8f6e-6a2a-4b57-9c1e-0f7f2e5d4a11
total_cents=2000
expires=2026-08-04T09:41:12.0000000Z
Call delegate_payment with: session_id above, merchant_id from search_products internal data, max_amount_in_cents=total_cents.
  • [BUYER MESSAGE] is written in the assistant's own voice. The model relays it (or a close paraphrase) straight to the buyer. It never contains IDs, field key names, or raw minor-unit amounts.
  • [INTERNAL AGENT DATA] carries the identifiers and the explicit next step. The model consumes it and must not show it.

This split is why the flow reads naturally in a chat window: the server, not your prompt, owns the buyer copy and the sequencing.

The call sequence

mermaid
sequenceDiagram
    autonumber
    actor Buyer
    participant Agent as AI assistant
    participant MCP as VantagePay MCP
    participant VP as VantagePay payments

    Buyer->>Agent: "Buy GHS 20 airtime for 024 123 4567"
    Agent->>MCP: search_products("airtime")
    MCP-->>Agent: product_reference + required fields
    Agent->>MCP: create_checkout(GHS, [line item])
    MCP-->>Agent: session_id, total_cents, missing fields?
    opt Required field missing
        Agent->>Buyer: "What's the mobile number?"
        Buyer->>Agent: "024 123 4567"
        Agent->>MCP: update_checkout(session_id, full basket)
    end
    Agent->>Buyer: "Card number, expiry and CVC please"
    Buyer->>Agent: Card details
    Agent->>MCP: delegate_payment(card, session_id, max_amount)
    MCP-->>Agent: vault_token (vt_...)
    Agent->>MCP: complete_checkout(session_id, vault_token)
    MCP->>VP: Queue payment
    MCP-->>Agent: queued - poll for status
    loop Every 3-5s until terminal
        Agent->>MCP: check_payment_status(session_id)
        MCP-->>Agent: processing / completed / failed
    end
    Agent->>Buyer: "Payment completed - confirmation 8c3f…"

Three rules the tool descriptions enforce, and which explain the agent behaviour you will observe:

  1. No pause between delegate_payment and complete_checkout. Asking the buyer to confirm again after they have already handed over their card wastes the 15-minute token window.
  2. Never announce success before check_payment_status says completed. complete_checkout succeeding only means the payment was accepted for processing.
  3. All required product fields before payment. The agent collects them up front, or fixes them with update_checkout - cards are never captured against an incomplete basket.

Amounts are always in minor units

This is the single most common integration mistake, so the server states it in every relevant tool and parameter description.

Buyer seesPass
GHS 1.00100
GHS 20.002000
GHS 50.005000
ZAR 199.9919999
$100.0010000

Multiply the displayed amount by 100. Passing 100 for "GHS 100" charges GHS 1.00.

The [BUYER MESSAGE] blocks always render amounts in major units, so the buyer never sees a cent value - and the agent is told to copy total_cents from the internal block into delegate_payment rather than recalculating it.

Products that need extra details

Many African payment products need more than a price: an airtime top-up needs a mobile number, a bill payment needs an account or meter number, a data bundle needs a network.

search_products declares these per product. For each field the agent receives:

ElementMeaning
keyThe exact dictionary key to use in extra_fields - for example mobileNumber.
nameThe label to show the buyer - for example "Mobile Number".
is_requiredWhether checkout can proceed without it.
Validation hintA fixed list of allowed values, a regex the value must match, or "free text".

If a required value is missing when create_checkout runs, the tool response:

  1. asks the buyer for exactly the missing values, in natural language, with the validation hint inline; and
  2. hands the agent a complete lineItemsJson template with the current prices preserved and <Mobile Number> style placeholders to fill.

Connect the server

The server is a remote MCP endpoint over HTTPS with a static bearer token. Every client below is configured with the same two values:

text
URL:    https://sandbox-api.vantagepay.dev/mcp
Header: Authorization: Bearer <your access token>

ChatGPT

ChatGPT connects to remote MCP servers as a custom connector in developer mode. Custom connectors are a paid-plan feature, and on Business / Enterprise / Edu workspaces an administrator has to enable developer mode before the option appears.

  1. Open Settings → Apps & Connectors → Advanced settings and turn on Developer mode.

  2. Go back to Settings → Apps & Connectors and choose Create.

  3. Fill in the connector:

    FieldValue
    NameVantagePay
    DescriptionSearch products and pay by card through VantagePay
    URLhttps://sandbox-api.vantagepay.dev/mcp
    AuthenticationToken / API key - paste your access token
  4. Save, then let ChatGPT scan the tools. All six VantagePay tools should be listed.

To use it in a conversation, open the + menu in the composer, pick Developer mode, and select the VantagePay connector. Then just ask for what you want to buy.

Write-action approvals

ChatGPT surfaces a confirmation prompt before write-style tool calls. During a purchase you will be asked to approve create_checkout, delegate_payment and complete_checkout the first time. Approving them for the session keeps the flow conversational.

Claude (web, desktop and mobile)

Claude custom connectors normally authenticate over OAuth. For a bearer-token server like VantagePay, use Request headers (currently in beta - ask Anthropic for access if you do not see the section):

  1. Open Settings → Connectors (or Admin settings → Connectors on Team / Enterprise) and choose Add custom connector.

  2. Enter the URL https://sandbox-api.vantagepay.dev/mcp.

  3. Expand Request headers, select the authorization header, and enter the value including the scheme:

    text
    Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

    Claude sends the value verbatim and adds no prefix, so omitting Bearer produces a 401.

  4. Mark the header Required and click Add.

Claude connects from Anthropic's cloud, not your machine

Even in Claude Desktop, a custom connector is brokered through Anthropic's infrastructure. The endpoint must be reachable from the public internet - a locally hosted or VPN-only build of the API will not connect. Use the sandbox host for testing.

If request-header authentication is not available on your plan, bridge the endpoint through mcp-remote in claude_desktop_config.json instead:

json
{
  "mcpServers": {
    "vantagepay": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://sandbox-api.vantagepay.dev/mcp",
        "--header",
        "Authorization:${AUTH_HEADER}"
      ],
      "env": {
        "AUTH_HEADER": "Bearer <your access token>"
      }
    }
  }
}
json
{
  "mcpServers": {
    "vantagepay": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://sandbox-api.vantagepay.dev/mcp",
        "--header",
        "Authorization: Bearer <your access token>"
      ]
    }
  }
}

The header is written without spaces around the colon on Windows, with the token supplied through env, because argument values containing spaces are mangled when the bridge is launched. Restart Claude Desktop fully after editing the file.

Claude Code

bash
claude mcp add --transport http vantagepay https://sandbox-api.vantagepay.dev/mcp \
  --header "Authorization: Bearer <your access token>"

Then confirm the connection and inspect the tools with /mcp. For a project-scoped setup, commit the equivalent entry to .mcp.json and keep the token out of source control by referencing an environment variable:

json
{
  "mcpServers": {
    "vantagepay": {
      "type": "http",
      "url": "https://sandbox-api.vantagepay.dev/mcp",
      "headers": {
        "Authorization": "Bearer ${VANTAGEPAY_TOKEN}"
      }
    }
  }
}

Codex CLI and the ChatGPT desktop app

Codex-based clients share ~/.codex/config.toml:

toml
[mcp_servers.vantagepay]
url = "https://sandbox-api.vantagepay.dev/mcp"
bearer_token_env_var = "VANTAGEPAY_TOKEN"

Export VANTAGEPAY_TOKEN before starting the client, then type /mcp to confirm the server is connected.

Any other MCP client

Point the client at the Streamable HTTP endpoint and add one header. Nothing else is required - the server advertises no resources or prompts, and needs no session initialisation beyond the standard MCP handshake.

json
{
  "type": "streamable-http",
  "url": "https://sandbox-api.vantagepay.dev/mcp",
  "headers": {
    "Authorization": "Bearer <your access token>"
  }
}

Test it in the sandbox

The sandbox runs the full payment pipeline against the VantagePay simulator, so a purchase behaves exactly as it will in production - it just moves no real money.

Test cards

Card numberBehaviour
4111111111111111Approves. Use any future expiry and any 3-digit CVC.

Expiry and CVC are not validated against a fixture, so 12 / 2030 / 123 is fine.

A first end-to-end purchase

Connect the server, then send a single message:

Show me what I can buy through VantagePay.

The assistant calls search_products and lists the catalogue in plain language, mentioning which details each product will need but not exposing product IDs.

Then buy something:

Buy GHS 20 of MTN airtime for 0241234567.

Expect this sequence:

  1. search_products - resolves the product and its required mobileNumber field.
  2. create_checkout - session opened, total 2000.
  3. The assistant asks for the card details (it will not have them yet).
  4. You reply: 4111111111111111, 12/2030, CVC 123.
  5. delegate_payment - card accepted, vault token minted.
  6. complete_checkout - payment queued, assistant says it is processing.
  7. check_payment_status × 2–4 - until completed.
  8. The assistant presents the confirmation number, amount and timestamp.

Scenarios worth exercising

ScenarioPromptWhat to verify
Missing product fieldBuy me some airtimeThe assistant asks for the mobile number before asking for a card, then calls update_checkout.
Basket changeActually make that GHS 50update_checkout is called with the full basket and the total changes to 5000.
Multiple line itemsBuy GHS 20 airtime for 0241234567 and GHS 30 of ECG units for meter 04512345678One session, two line items, one card authorisation for the combined total.
Amount disciplineBuy GHS 100 of airtime for 0241234567The confirmation reads 100.00 GHS, not 1.00 GHS - proof the agent passed 10000.
Expired sessionStart a checkout, wait past the session TTL, then continueThe tool reports the session was not found and the assistant offers to start again.
Declined cardUse a card number other than the approving test cardcheck_payment_status reports failed and the assistant offers a different card.

Verifying on the VantagePay side

The confirmation number the assistant reports is the transaction batch reference. Use it to inspect the payment through the regular APIs:

Lifetimes and limits

ItemValueNotes
Checkout session TTL30 minutesSessions live in cache; an expired session cannot be updated or completed.
Vault token TTL15 minutesNever outlives the session. Deleted on first use.
Vault token reuseNot permittedRedeeming it twice fails - mint a new one with delegate_payment.
Terminal statusescompleted, canceled, requires_escalationA terminal session cannot be updated, completed or cancelled again.
ConcurrencyOne writer per sessionUpdates and completions take a distributed lock; a concurrent call is rejected rather than racing.

Troubleshooting

SymptomCauseFix
Connector saves but every tool call returns 401Token missing, expired, or the Bearer prefix was omittedRe-issue the access token and re-enter the header value including Bearer and the space.
"Failed to connect" during tool scanURL is not the MCP endpoint, or the host is not publicly reachableConfirm the URL ends in /mcp and that you are using the sandbox host, not a local build.
Charged 1/100th of the expected amountMajor units passed instead of minor unitsThe amount must be multiplied by 100. Have the agent copy total_cents from the tool response rather than recompute it.
Assistant announces success, then the payment failscheck_payment_status was not polled to a terminal stateRemind the model that complete_checkout only queues the payment; the flow is not finished until the status is completed.
"Session not found or has expired"The session TTL elapsed, or the wrong session ID was passedStart a new checkout with create_checkout.
Allowance max_amount must be greater than zeromaxAmountInCents was 0 or negativePass the session total in minor units.
Session total N exceeds allowance max_amount MThe basket grew after the card was authorisedCall delegate_payment again for the new total, then complete_checkout with the new token.
Assistant loops asking for the same product fieldThe wrong extra_fields key was usedKeys are case-sensitive and must match the key from search_products exactly (for example mobileNumber, not phone).

Security notes

  • Card data never persists. Card details are encrypted, held only for the life of the single-use vault token, and deleted the moment the token is redeemed. The agent only ever handles the opaque vt_… value.
  • Every authorisation is bounded. A vault token is valid for one session, one merchant, one currency, one maximum amount and 15 minutes. It cannot be redirected to another basket.
  • Tokens are bearer credentials. Scope them tightly, keep them out of source control and connector descriptions, and rotate them if a workspace connector is shared.
  • Prompt injection is a real risk. Product names and descriptions reach the model as text. Keep your catalogue content clean, and remember that any MCP server you connect can influence assistant behaviour.
  • 3-D Secure is bypassed for agentic payments. That is what makes an in-conversation purchase possible, and it shifts liability accordingly. Review your card-scheme obligations before enabling agentic commerce in production.
  • ACP REST API reference - the same capabilities as HTTP endpoints, for when you orchestrate checkout in your own code.
  • Authentication - how to obtain and refresh the access token.
  • Payment status - inspect the transaction batch behind a completed order.
  • Products - manage the catalogue that search_products reads from.

Payments for Africa