Skip to main content
Glama

create_payment_link

Generate payment or checkout links on Stream for one-time or recurring products with item details, coupons, and customizable parameters.

Instructions

Create a new payment / checkout link on Stream.

items is a list of objects, each containing:

  • product_id (str, required)

  • quantity (int ≥ 1, optional, default 1)

  • coupons (list[str], optional)

You cannot mix one-time and recurring products in the same link.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
nameYes
itemsYes
descriptionNo
currencyNoSAR
valid_untilNo
max_number_of_paymentsNo
organization_consumer_idNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • The main create_payment_link tool handler function. It accepts parameters (name, items, description, currency, valid_until, max_number_of_payments, organization_consumer_id), creates a CreatePaymentLinkRequest, gets the API client, and makes a POST request to /api/v2/payment_links. Handles StreamAPIError exceptions.
    async def create_payment_link(
        name: str,
        items: list[dict],
        description: str | None = None,
        currency: str = "SAR",
        valid_until: str | None = None,
        max_number_of_payments: int | None = None,
        organization_consumer_id: str | None = None,
        ctx: Context = None,  # type: ignore[assignment]
    ) -> dict[str, Any]:
        """Create a new payment / checkout link on Stream.
    
        *items* is a list of objects, each containing:
          - product_id (str, required)
          - quantity   (int ≥ 1, optional, default 1)
          - coupons    (list[str], optional)
    
        You **cannot** mix one-time and recurring products in the same link.
        """
        body = CreatePaymentLinkRequest(
            name=name,
            items=items,  # type: ignore[arg-type]
            description=description,
            currency=currency,
            valid_until=valid_until,
            max_number_of_payments=max_number_of_payments,
            organization_consumer_id=organization_consumer_id,
        )
        client = await get_client(ctx)
        try:
            return await client.post(_BASE, body.model_dump(exclude_none=True))
        except StreamAPIError as exc:
            return _err(exc)
  • Pydantic BaseModel for CreatePaymentLinkRequest with field validation. Defines the structure for creating payment links including name, items, description, currency, coupons, max_number_of_payments, valid_until, success_redirect_url, failure_redirect_url, and organization_consumer_id.
    class CreatePaymentLinkRequest(BaseModel):
        """Request body for creating a new payment link."""
    
        name: str = Field(..., description="Display name for the payment link.")
        items: list[PaymentLinkItem] = Field(..., min_length=1, description="Line-items (product + quantity + coupons). Cannot mix one-time and recurring products.")
        description: str | None = Field(default=None, description="Optional description shown on the checkout page.")
        currency: str = Field(default="SAR", description="ISO-4217 currency code.")
        coupons: list[str] | None = Field(default=None, description="Coupon IDs to apply.")
        max_number_of_payments: int | None = Field(default=None, description="Maximum number of payments allowed.")
        valid_until: str | None = Field(default=None, description="ISO-8601 expiry timestamp. Null = never expires.")
        success_redirect_url: str | None = Field(default=None, description="URL to redirect to after successful payment.")
        failure_redirect_url: str | None = Field(default=None, description="URL to redirect to after failed payment.")
        organization_consumer_id: str | None = Field(default=None, description="Pre-assign a customer to this link.")
  • PaymentLinkItem nested schema defining individual line items with product_id (required), quantity (≥1), and optional coupons list.
    class PaymentLinkItem(BaseModel):
        """A single line-item inside a payment link."""
    
        product_id: str = Field(..., description="ID of the product to include.")
        quantity: int = Field(ge=1, description="Number of units (≥ 1).")
        coupons: list[str] = Field(default_factory=list, description="Coupon IDs to apply to this item.")
  • The register function that registers create_payment_link as an MCP tool using @mcp.tool decorator. This function is called from the tools/__init__.py module.
    def register(mcp: FastMCP) -> None:
        """Register all payment-link tools on *mcp*."""
    
        @mcp.tool
        async def create_payment_link(
            name: str,
            items: list[dict],
            description: str | None = None,
            currency: str = "SAR",
            valid_until: str | None = None,
            max_number_of_payments: int | None = None,
            organization_consumer_id: str | None = None,
            ctx: Context = None,  # type: ignore[assignment]
        ) -> dict[str, Any]:
            """Create a new payment / checkout link on Stream.
    
            *items* is a list of objects, each containing:
              - product_id (str, required)
              - quantity   (int ≥ 1, optional, default 1)
              - coupons    (list[str], optional)
    
            You **cannot** mix one-time and recurring products in the same link.
            """
            body = CreatePaymentLinkRequest(
                name=name,
                items=items,  # type: ignore[arg-type]
                description=description,
                currency=currency,
                valid_until=valid_until,
                max_number_of_payments=max_number_of_payments,
                organization_consumer_id=organization_consumer_id,
            )
            client = await get_client(ctx)
            try:
                return await client.post(_BASE, body.model_dump(exclude_none=True))
            except StreamAPIError as exc:
                return _err(exc)
  • The get_client helper function used by the handler to obtain a StreamClient instance. Supports both local mode (shared client from lifespan) and remote mode (per-user client from Bearer token).
    async def get_client(ctx: "Context") -> StreamClient:
        """Return a :class:`StreamClient` for the current request.
    
        Resolution order:
    
        1. **Lifespan client** — used in local / stdio mode where a single
           ``STREAM_API_KEY`` is set as an environment variable.
        2. **Per-user client** — used in remote mode where each user passes
           their own API key as a Bearer token and (optionally) a custom
           base URL via the ``X-Stream-Base-URL`` header.
        """
        # ── 1. Local mode: shared client from server lifespan ─────────────
        shared_client = ctx.lifespan_context.get("client")
        if shared_client is not None:
            return shared_client
    
        # ── 2. Remote mode: per-user client from Bearer token ─────────────
        api_key = current_api_key.get()
        if not api_key:
            raise StreamError(
                "No Stream API key found. "
                "In local mode, set the STREAM_API_KEY env var. "
                "In remote mode, pass your key as a Bearer token in the Authorization header."
            )
    
        base_url = current_base_url.get() or settings.stream_base_url
        cache_key = f"{api_key}::{base_url}"
    
        if cache_key not in _client_cache:
            client = StreamClient(
                api_key=api_key,
                base_url=base_url,
                timeout=settings.stream_timeout,
                max_retries=settings.stream_max_retries,
            )
            await client.__aenter__()
            _client_cache[cache_key] = client
            logger.info(
                "Created cached StreamClient for remote user (key=…%s, base=%s)",
                api_key[-4:], base_url,
            )
    
        return _client_cache[cache_key]
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It mentions a constraint about mixing product types, which is useful, but fails to describe critical behaviors such as authentication needs, rate limits, what happens upon creation (e.g., link generation, storage), or error conditions. For a mutation tool with zero annotation coverage, this is a significant gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized and front-loaded, starting with the core purpose. The bullet points for 'items' are well-structured, and the constraint about mixing products is clearly emphasized. However, the lack of complete parameter coverage or additional context slightly reduces efficiency.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (7 parameters, mutation operation) and no annotations, the description is moderately complete. It explains the 'items' parameter well and includes a key constraint, but omits details on other parameters, behavioral traits, and usage context. The presence of an output schema mitigates some need to explain return values, but overall gaps remain.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description adds meaningful semantics for the 'items' parameter by detailing its structure (list of objects with product_id, quantity, coupons) and constraints (quantity ≥ 1, default 1). With 0% schema description coverage and 7 parameters, this partially compensates for the schema's lack of documentation, though it doesn't cover other parameters like 'name' or 'currency'.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Create a new payment / checkout link on Stream.' It specifies the verb ('Create') and resource ('payment / checkout link'), and the platform ('Stream'). However, it doesn't explicitly differentiate from sibling tools like 'create_invoice' or 'create_product', which would require a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides implied usage guidance by stating 'You **cannot** mix one-time and recurring products in the same link,' which hints at when not to use it. However, it lacks explicit guidance on when to choose this tool over alternatives like 'create_invoice' or 'create_product', and doesn't mention prerequisites or context for usage.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/streampayments/stream'

If you have feedback or need assistance with the MCP directory API, please join our Discord server