Skip to main content
Glama
README.md
# wayl-mcp

[![CI](https://github.com/muthanii/waylMCP/actions/workflows/ci.yml/badge.svg)](https://github.com/muthanii/waylMCP/actions/workflows/ci.yml)
[![waylMCP MCP server](https://glama.ai/mcp/servers/muthanii/waylMCP/badges/score.svg)](https://glama.ai/mcp/servers/muthanii/waylMCP)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)

An [MCP](https://modelcontextprotocol.io) server for the [Wayl](https://wayl.io)
payments API — take payments online in Iraq from an AI assistant.

Ask your assistant to *"charge 25,000 dinars for a consultation"* and it creates the
payment link, hands you the checkout URL, and can tell you later whether the customer
actually paid. Works for anything you sell: physical goods, digital downloads,
services, tickets, invoices.

Wayl is an Iraqi payment gateway. All amounts are in Iraqi Dinar (IQD).

[![waylMCP MCP server](https://glama.ai/mcp/servers/muthanii/waylMCP/badges/card.svg)](https://glama.ai/mcp/servers/muthanii/waylMCP)

## Setup

You need an API key. Wayl's guide says to email `jisr@wayl.io` to request a merchant
token; their API reference says it is in your merchant dashboard. Try the dashboard
first, then email. Your store must be **verified** before it can create links.

```bash
uv sync
```

Then set the key:

```bash
export WAYL_API_KEY="your-merchant-token"
```

Check it works:

```bash
uv run python -c "import asyncio, wayl_mcp.server as s; print(asyncio.run(s.verify_auth_key()))"
```

## Connecting it

### Claude Code

```bash
claude mcp add wayl --env WAYL_API_KEY=your-merchant-token -- uv run --directory /absolute/path/to/wayl_MCP wayl-mcp
```

### Claude Desktop

In `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "wayl": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/wayl_MCP", "wayl-mcp"],
      "env": {
        "WAYL_API_KEY": "your-merchant-token",
        "WAYL_ENV": "test"
      }
    }
  }
}
```

Use an absolute path — the server is launched from an arbitrary working directory.

## Configuration

| Variable | Default | Purpose |
|---|---|---|
| `WAYL_API_KEY` | — | **Required.** Merchant token, sent as `X-WAYL-AUTHENTICATION`. |
| `WAYL_ENV` | `test` | Default environment for new links: `test` or `live`. |
| `WAYL_BASE_URL` | `https://api.thewayl.com` | API host. |
| `WAYL_WEBHOOK_URL` | — | Default webhook URL for new links. |
| `WAYL_WEBHOOK_SECRET` | — | Default webhook signing secret (10–255 chars). |
| `WAYL_REDIRECT_URL` | — | Where buyers land after paying. |
| `WAYL_REFERENCE_PREFIX` | `order` | Prefix for generated order IDs. |
| `WAYL_TIMEOUT` | `30` | HTTP timeout in seconds. |

**`WAYL_ENV` defaults to `test`** so nothing moves real money until you opt in. Set it
to `live` when you are ready to actually sell, or pass `env="live"` per call.

While in test mode, `check_order_paid` and `parse_webhook` report `paid: true` for a
completed sandbox checkout but `safeToFulfil: false` — the payment is simulated, so the
order should not be fulfilled. Branch on `safeToFulfil`, not `paid`.

## Tools

**Selling**

| Tool | Does |
|---|---|
| `sell_item` | Create a checkout link for a simple sale, with the price breakdown filled in. |
| `check_order_paid` | Answer whether an order is paid and safe to fulfil. |
| `create_payment_link` | Create a payment link with full control over every field. |

**Links**

| Tool | Does |
|---|---|
| `get_payment_link` | Fetch one link and its status. |
| `list_payment_links` | List links, newest first, filterable by status. |
| `get_payment_links_batch` | Look up to 100 links at once; reports which were missing. |
| `invalidate_payment_link` | Cancel an unpaid link. |
| `invalidate_payment_link_if_pending` | Cancel it only if still pending. |

**Products**

| Tool | Does |
|---|---|
| `list_products` | List your Wayl catalogue (Digital, Physical, Service). |
| `get_product` | Fetch one product's details. |

**Refunds**

| Tool | Does |
|---|---|
| `create_refund` | Request a refund. Needs a 100+ character justification. |
| `list_refunds` | List refund requests. |
| `get_refund` | Fetch one refund. |
| `cancel_refund` | Withdraw a refund still in `Requested`. |

**Webhooks and diagnostics**

| Tool | Does |
|---|---|
| `parse_webhook` | Verify a webhook's signature and report whether the order is paid. |
| `verify_webhook` | Signature check alone. |
| `verify_auth_key` | Confirm the API key works. |
| `wayl_status` | Show how the server is configured, without calling the API. |

Read-only tools are marked `readOnlyHint`; refunds and invalidations are marked
`destructiveHint` so your client can ask before running them.

## Taking a payment

Creating the link:

> Sell "Wireless keyboard" for 30000 IQD with 5000 delivery

`sell_item` builds the line items, generates a unique reference ID, and returns a
checkout URL like `https://checkout.thewayl.com/pay/I94F590I`. Send that to the
customer. It works the same for a service, a ticket or a digital download — set
`delivery_fee=0` when nothing ships.

For full control over webhooks, redirects and custom line items, use
`create_payment_link` instead.

Finding out whether they paid — either poll:

> Has order order-wireless-keyboard-a1b2c3 been paid?

or receive a webhook. Set `webhookUrl` and `webhookSecret` when creating the link, then
pass each incoming request to `parse_webhook`, which verifies the signature and tells
you whether to fulfil.

## Webhooks

Wayl signs each delivery with HMAC-SHA256 over the **raw request body**, sending the
hex digest in the `x-wayl-signature-256` header.

Three things that break integrations:

1. **Hash the raw bytes.** `json.dumps(json.loads(body))` changes whitespace and key
   order, so the digest will not match. Verify before you parse.
2. **Wayl sends `Content-Type: text/plain`**, so JSON body parsers may hand you an
   empty body. Read the raw body yourself.
3. **Deduplicate on the payload's `id`.** There is no timestamp in the signature, so a
   captured request replays forever — and Wayl retries on timeout, so duplicates happen
   in normal operation too.

The webhook reports `paymentStatus: "Paid"`, which is *not* one of the eight link
statuses the REST API uses. `parse_webhook` handles that distinction.

## Development

```bash
uv run pytest
```

```bash
uv run ruff check src tests
```

See [CLAUDE.md](CLAUDE.md) for architecture notes and the API's sharp edges.

## Licence

MIT

TDQS

A4.6/5.0

Scored across 18 tools

Disambiguation5/5

Every tool has a distinctly defined purpose, and the descriptions carefully cross-reference related tools (e.g., get_payment_link vs get_payment_links_batch vs check_order_paid, invalidate_payment_link vs invalidate_payment_link_if_pending). This makes selection unambiguous for an agent.

Naming Consistency4/5

Most tools follow a clear verb_noun pattern (create_, get_, list_, invalidate_, cancel_, verify_, parse_). The only outlier is wayl_status, which could have been get_status, but it remains understandable.

Tool Count4/5

18 tools is on the heavier side, but the server covers payment links, products, refunds, webhooks, and configuration—each area justifies several tools. The count feels appropriate for the scope, though some consolidation might be possible.

Completeness4/5

The core workflow (create payment link, monitor payment, verify webhook, refund) is fully covered. Minor gaps: product tools are read-only only, and there is no update operation for payment links, but these are not essential for the server's stated purpose of selling books.

Maintenance

ActivitySlowing
ResponsivenessNo issues