# Stream App API Documentation > Complete API documentation and guides for Stream App ## Quick Start for LLMs > Read this section first. It is written for AI agents and coding assistants integrating with Stream (streampay.sa), a Saudi payment platform for payment links, invoices, subscriptions, installments, coupons, and a hosted customer billing portal. Everything below is derived from the guides and API reference listed in the Table of Contents further down. ### Mental model (read before writing any code) - **Default flow:** Your server -> `POST /api/v2/payment_links` -> redirect the customer (or embed) to the returned `url` -> Stream hosted checkout -> customer lands on your `success_redirect_url` / `failure_redirect_url` -> **you verify server-side** and/or receive webhooks. - **Payment links are the recommended way to collect money.** The invoice and subscription create endpoints say so themselves: "For most use cases, use the Checkout/Payment Link API instead." Do not default to `POST /invoices` or `POST /subscriptions`. - **Entities are created only after a successful payment.** A paid link creates: - one-time products -> an **invoice** + **payment(s)** - recurring products -> a **subscription** + initial **invoice** + **payment** - A failed attempt creates nothing (no invoice, payment, or subscription). - **Recurrence lives on the product, not the link.** Create the product as one-time or recurring first, then reference it by `product_id`. - **"Consumer" = "customer".** The API says consumer; the dashboard says customer. The consumer `id` is what you pass as `organization_consumer_id`. ### Base URL and authentication - Base URL: `https://stream-app-service.streampay.sa/api/v2` - Header on every request: `x-api-key: ` - Build the token: `echo -n 'api-key:api-secret' | base64` - Create the key pair in the dashboard under Settings -> API Keys. The secret is shown at creation; store it like a password. - Never put the key pair or token in browser/mobile code. All calls go through your backend. - Optional header `x-branch-id: ` selects the branch. If omitted, the key's default branch is used. Keys only reach branches they were granted (or all branches if created with `is_all_branches`). Unauthorized branch -> 4xx. - The MCP server uses a different header: `Authorization: Bearer ` (see MCP section). ### The five-step integration **1. (Optional) Create a consumer** - `POST /api/v2/consumers` Only `name` is required. Recommended: `phone_number` (E.164, e.g. `+966501234567`) or `email` so Stream can notify them, plus `external_id` for your own ID. ```bash curl -X POST https://stream-app-service.streampay.sa/api/v2/consumers \ -H "x-api-key: $STREAM_TOKEN" -H "Content-Type: application/json" \ -d '{"name":"Ahmad Ali","email":"ahmad@example.com","phone_number":"+966501234567","external_id":"cust_12345"}' ``` Returns the consumer; keep `id`. Duplicate phone/email -> `DUPLICATE_CONSUMER`: list consumers and reuse the existing `id`. **2. Have a product** - `POST /api/v2/products` (one-time or recurring). Keep its `id`. A product can hold one active price per currency (`POST /products/{product_id}/prices` adds another currency). **3. Create the payment link** - `POST /api/v2/payment_links` ```bash curl -X POST https://stream-app-service.streampay.sa/api/v2/payment_links \ -H "x-api-key: $STREAM_TOKEN" -H "Content-Type: application/json" \ -d '{ "name": "Order 789", "items": [{"product_id": "PRODUCT_UUID", "quantity": 1}], "contact_information_type": "PHONE", "currency": "SAR", "max_number_of_payments": 1, "organization_consumer_id": "CONSUMER_UUID", "success_redirect_url": "https://example.com/success", "failure_redirect_url": "https://example.com/failure", "custom_metadata": {"order_id": "789"} }' ``` - Response includes `url`, `id`, `status` (e.g. `ACTIVE`), `amount` (decimal string like `"2500.00"`), `currency`. Redirect the customer to `url`. - Omit `organization_consumer_id` for a shareable link: each payer enters their own details and a consumer is auto-created. Use a higher `max_number_of_payments` for these (docs example: 100). - Set `organization_consumer_id` + `max_number_of_payments: 1` for a link tied to one known customer; the checkout skips the details form. **4. Handle the redirect, then verify.** Stream appends query params (`status`, `message`, `payment_link_id`, and on success `invoice_id`, `payment_id`, `consent_id`, `organization_consumer_id`). **Do not trust them.** Fetch `GET /api/v2/invoices/{invoice_id}` and check `status`, `amount`, `currency` against your own order before fulfilling. **5. Confirm asynchronously with webhooks** (recommended) and/or poll `GET /api/v2/payments/{id}` and `GET /api/v2/subscriptions/{id}`. The `status` field on the fetched object is the source of truth. ### Embedded checkout (instead of redirect) Create the payment link on your server, pass only the `url` to the page: ```html
``` Optional: `minHeightPx` (280), `maxHeightPx` (920), `vhCapPx` (800), `vhFraction` (0.7). Apple Pay in embedded mode requires hosting Stream's domain-association file at `/.well-known/apple-developer-merchantid-domain-association` on your domain and contacting Stream support to register you. ### Webhooks in 60 seconds - Register endpoints in the dashboard (Settings -> Webhooks) and pick event types. There is also a `POST /api/v2/webhooks` endpoint. - Body shape: `event_type`, `entity_type`, `entity_id`, `entity_url`, `status`, `data`, `timestamp`. `data.metadata` echoes your `custom_metadata`. - Headers: `X-Webhook-Event`, `X-Webhook-Entity-Type`, `X-Webhook-Entity-ID`, `X-Webhook-Timestamp`, `X-Webhook-Signature` (`t={timestamp},v1={signature}`). - **Verify the signature:** HMAC-SHA256 over the exact string `"{timestamp}.{raw_request_body}"` using the webhook's `secret_key`, hex digest, constant-time compare to `v1`. Use the **raw bytes** of the body, not re-serialized JSON. ```python import hmac, hashlib def verify(secret: str, raw_body: bytes, header: str) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) msg = f"{parts['t']}.{raw_body.decode()}" expected = hmac.new(secret.encode(), msg.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts["v1"]) ``` - Respond with any 2xx quickly. Non-2xx triggers up to 5 retries at roughly 5 min, 30 min, 2 h, 6 h, 12 h; after that the delivery is marked failed. ### Testing - Use the sandbox environment with Moyasar test cards. Name: any two words. Expiry: any future date. CVC: any 3 digits (4 for AMEX). - Success: Mada `4201320111111010`, Visa `4111111111111111`, Mastercard `5421080101000000`, AMEX `340000000900000`. - Insufficient funds: Mada `4201320000311101`, Visa `4123120001090000`, Mastercard `5457210001000092`, AMEX `340033000000000`. - Generic decline: Mada `4201321234411220`, Visa `4123120001090109`. - 3DS failure cases exist for Visa only (see Testing Cards guide). - **Apple Pay in sandbox is driven by amount:** 200.00-300.00 SAR succeeds; 1000.00-1700.00 SAR hits assorted failures; **any other amount fails.** Do not use random amounts when testing Apple Pay. --- ## Tips and Tricks for LLMs ### Hard rules (violating these causes real bugs or lost money) 1. **Never mix one-time and recurring products in one payment link.** Split into two links tied to one internal order (store a shared order reference in `custom_metadata` or your DB), run them sequentially, and mark the order complete only after both succeed. 2. **Never fulfill an order from redirect query params alone.** Verify via `GET /invoices/{invoice_id}` (status, amount, currency) or a verified webhook. 3. **Never expose the key pair, the Base64 token, or a webhook `secret_key` client-side.** Embedded checkout only needs the link `url`. 4. **Always verify `X-Webhook-Signature`** and reject missing or invalid ones. 5. **Make webhook handlers idempotent.** Retries and multi-event flows mean you will see duplicates and out-of-order delivery. Key on `entity_id` + `event_type`. 6. **Do not use the redirect `id` param as a Stream ID.** It is the payment gateway's ID, for support debugging only. Use `payment_id` or `invoice_id`. ### Choosing the right endpoint | Goal | Use | |---|---| | Collect a one-off payment | `POST /payment_links` with one-time product(s) | | Start a subscription | `POST /payment_links` with recurring product(s) | | Take payment via your own page | `POST /payment_links` + Embedded Checkout | | Record money received offline (cash, bank transfer, etc.) | Create invoice, then `POST /payments/{id}/mark-paid` | | Send customer to self-service billing | `POST /consumer_portal/sessions` | | Charge a saved card on demand | `POST /payments/auto-charge-on-demand/{payment_id}` (restricted; only some orgs) | | Pause billing | `POST /subscriptions/{id}/freeze` | | Reverse a payment | `POST /payments/{id}/refund` | ### Consumers - Check for an existing consumer before creating one (match by `external_id`, or `GET /consumers?search_term=...`) to avoid `DUPLICATE_CONSUMER`. - Phone numbers must be E.164. `+966501234567`, not `0501234567`. - `communication_methods` accepts `WHATSAPP`, `EMAIL`, `SMS`. Provide at least a phone or email, otherwise Stream cannot notify the customer. - `DELETE` on a consumer is a soft delete (`is_deleted`). ### Payment links - `contact_information_type` is `PHONE` or `EMAIL` (shown in docs examples) and controls which contact the checkout collects/uses for login. - Put your own references in `custom_metadata` (arbitrary JSON, nested allowed). It comes back in webhook `data.metadata`, which is the cleanest way to join Stream events to your orders. - `language=en` or `language=ar` as a query param on the link URL switches the checkout UI language only. Default is Arabic (or the org's configured language). - Installments: pass `payment_methods: {"installment": true|false}` on the link to override the org default. Omit `payment_methods` to inherit the org setting. - Link coupons and status are managed after creation with `PATCH`-style endpoints: Update Payment Link Coupons, Update Payment Link Status. To retire a link, update its status rather than deleting. - Failed payment-link attempts produce **no** invoice/payment/subscription. To catch these, subscribe to `PAYMENT_LINK_PAY_ATTEMPT_FAILED`, not `PAYMENT_FAILED`. - Redirect `status` values: `paid`, `pending`, `failed`, `failed_internal_error`. `failed` = definite decline, let the payer retry. `failed_internal_error` = unclear (timeout etc.); money may or may not have moved, so tell the payer to contact support and reconcile via the API before retrying or refunding. ### Installments - Customers choose 2, 3, or 4 installments at checkout. **Installments are disabled automatically for recurring products.** - Each installment is its own payment with its own due date. Default split period is monthly; it cannot be changed via API or dashboard (contact support). - Minimum is **100 SAR per installment**. A 250 SAR order allows 2 installments but not 3. This minimum is also support-only to change. - Rounding remainder goes to the **last** installment (999.98 / 3 -> 333.33, 333.33, 333.32). - Webhooks: one `PAYMENT_SUCCEEDED` per installment paid; `INVOICE_COMPLETED` only fires once **every** installment is paid. - Invoice-level installment settings are configured in the dashboard (invoices are created from the UI per the guide); payment-link defaults live in Settings -> Payment Settings. ### Subscriptions - Cancel (`POST /subscriptions/{id}/cancel`): an `ACTIVE` subscription is **scheduled to cancel at period end** and stays active until then. `INACTIVE`, `TRIALING`, and `TRIAL_PENDING` subscriptions cancel **immediately**. Pass `cancel_related_invoices` to also cancel ongoing invoices. Undo a pending cancellation with the Uncancel endpoint. - Update (`PUT /subscriptions/{id}`): - Items, subscription-level coupons, and recurring interval changes are **deferred to `current_period_end`** as a "pending change". The response includes `pending_change` with `next_invoice_preview` and a per-line diff. - If the next-cycle invoice already exists and is unpaid, it is cancelled and reissued at the new amount. - Description, auto-cancel cycle count (`until cycle number`), and payment-method overrides apply **immediately**. - You cannot change the payer. Cancel and create a new subscription instead. - Revert a scheduled change with `DELETE /subscriptions/{id}/pending-change` (returns 200 even if nothing was scheduled). - Freeze pauses **invoice generation** for a date range and auto-resumes afterward. Freezes have their own list / update / delete endpoints. - Renewal webhook sequence: `INVOICE_CREATED` -> `PAYMENT_SUCCEEDED` -> `INVOICE_COMPLETED` (plus `SUBSCRIPTION_CYCLE_RENEWED_SUCCESSFULLY`). On `INVOICE_COMPLETED`, check `subscription_id` in the payload to recognize a renewal. Failed automatic renewals emit `SUBSCRIPTION_CYCLE_RENEWAL_FAILED`. - Plan-change event family: `SUBSCRIPTION_PLAN_CHANGE_SCHEDULED` (deferred), `..._CHANGED` (deferred change applied), `..._CHANGE_CANCELED`, `..._CHANGE_INVOICE_REISSUED`, and `SUBSCRIPTION_PLAN_UPDATED` (immediate change such as seats, add-ons, cycle count). ### Payments, refunds, and manual payments - Multiple refunds per payment are supported. Partial refund -> payment status `PARTIALLY_REFUNDED` + `PAYMENT_PARTIALLY_REFUNDED`. Cumulative total reaches the payment amount -> `REFUNDED` + `PAYMENT_REFUNDED`. **The invoice status does not change on refund.** Subscribe to both refund events if you handle partials. - Webhook refund payloads carry `amount_refunded` (this refund) and `total_amount_refunded` (cumulative). - If a payment belongs to a shared charge, set `payment_is_part_of_related_set: true` on the refund request to spread the refund across the related payments in invoice order. - Refund errors: `PAYMENT_REFUNDED_ALREADY` (duplicate, safe to treat as done), `PAYMENT_REFUNDED_FAILED` (processor failure, retryable later). - `mark-paid` supports methods `CASH`, `BANK_TRANSFER`, `CARD`, `QURRAH`. Omit `amount` (or equal to payment amount) for full. A partial amount reduces the original payment, marks it `SUCCEEDED`, and creates a new `PENDING` `SPLIT_REMAINDER` payment for the rest; the remainder must be more than 1 in the payment currency, and partial is not allowed while `UNDER_REVIEW`. The invoice completes only when every payment is paid. - Invoice in-place edits (`PATCH /invoices/{id}/inplace`) are for small changes such as description or scheduled date. A finalised invoice rejects actions with `INVOICE_FINALISED`. - Products and coupons used by a finalized invoice cannot be edited (`PRODUCT_USED_IN_FINALIZED_INVOICE`, `COUPON_USED_IN_FINALIZED_INVOICE`). Create a new one instead. Deleting a product price is a soft delete (archive). ### Lists, pagination, and filtering - Query params: `page` (starts at 1), `limit` (max 100), `sort_field` (default `created_at`), `sort_direction` (`asc`/`desc`, default `desc`), `search_term` (free text across names/IDs/descriptions). - Array filters repeat the key: `statuses=PENDING&statuses=PROCESSING`. In Python `requests`, pass a list. Resource-specific filters exist (e.g. `invoice_id` on payments). - Response: `{"data": [...], "pagination": {"total_count", "max_page", "current_page", "limit", "has_next_page", "has_previous_page"}}`. Loop on `has_next_page`; use `limit=100` to minimize calls. Note: the guide describes `total_count` as the count on the current page, so rely on `has_next_page` / `max_page` rather than assuming it is a global total. - When you apply filters, pagination metadata reflects the filtered set. ### Errors - Handled errors use `{"error": {"code": "...", "message": "...", "additional_info": "..."}}`. Branch on `code`, never on `message`. - Validation failures (HTTP 422) can instead return FastAPI-style `{"detail": [{"loc": [...], "msg": "...", "type": "..."}]}`. Handle **both** shapes. - Status mapping: 400 bad request, 401 unauthorized, 403 forbidden / permission, 404 not found, 410 gone, 422 unprocessable or duplicate. - Codes you should handle explicitly: `INVALID_PARAMETERS`, `DUPLICATE_CONSUMER`, `DUPLICATE_PAYMENT`, `PHONE_ALREADY_REGISTERED`, `INVOICE_FINALISED`, `PAYMENT_REFUNDED_ALREADY`, `PAYMENT_REFUNDED_FAILED`, `PERMISSION_FORBIDDEN`, `PRODUCT_USED_IN_FINALIZED_INVOICE`, `COUPON_USED_IN_FINALIZED_INVOICE`, `STREAM_ERROR`, `STREAM_UNKNOWN_ERROR`. On `STREAM_ERROR` / `STREAM_UNKNOWN_ERROR`, log `additional_info` and escalate to support rather than blind-retrying a money-moving call. ### Customer portal - Backend calls `POST /api/v2/consumer_portal/sessions` with `organization_consumer_id` (HTTP 201 -> `url`). The token is **short-lived and single-use**, so generate a fresh session each time the customer clicks "Manage billing". Do not cache or email the URL. - Optional body fields: `return_url` (a back-link hint only; Stream does **not** auto-redirect) and `subscription_id` (open directly on one subscription). The URL can be used for a redirect or an iframe. - The portal can only offer plan switches and add-ons if the dashboard catalog is configured: **product switch groups** (only members of the same group can replace each other; mixed MONTHLY/YEARLY allowed) and **add-on mappings** (which base products make an add-on eligible), plus the Customer Portal permission toggles. If the portal shows nothing to switch, the cause is configuration, not the API. - Subscriptions with subscription-level coupons generally cannot be edited from the portal. Proration is computed by Stream and excludes the current day; do not reimplement it. ### Handling your own customer notifications By default Stream notifies consumers (confirmations, invoice updates, subscription changes). To own all messaging: (1) ask Stream support to enable the setting that blocks default consumer notifications, (2) use webhooks for event-driven messages, (3) poll the List Payments / Invoices / Subscriptions endpoints with filters for scheduled messages like reminders and overdue notices. You cannot self-serve step 1. ### Branches Requests are scoped to one branch. Use `x-branch-id` to target a branch; without it the key's default branch is used. If resources seem "missing", you are probably querying the wrong branch. ### MCP server (for AI agents) - Hosted at `https://mcp.streampay.sa/mcp`. Config key `"url"`, plus `"headers": {"Authorization": "Bearer "}` for operational tools. - The resource `stream://docs/openapi` exposes the **full, current OpenAPI spec and needs no API key**. Read it before guessing request fields; this documentation set links endpoint pages whose rendered Markdown does not include the full request/response schemas. - Tool groups: payment links, customers, products, coupons, invoices (create, list, get, send, void), payments (list, get, refund). Tool names (e.g. `create_payment_link`, `refund_payment`) are not the same as REST paths. Note that some MCP descriptions (for example ZATCA-compliant invoices) are tool-level descriptions, so confirm behavior in the OpenAPI spec. ### SDKs TypeScript SDK (`StreamSDK.init(process.env.STREAM_API_KEY!)`) and an Express SDK (a `Checkout({ apiKey, ... })` route handler). Full docs live in their GitHub repositories, linked at the bottom of the Table of Contents. SDKs take the same `x-api-key` token. ### Gotchas and doc inconsistencies to be aware of - **The Installments link in the Table of Contents is malformed** (`https://docs.streampay.sa/docs/guides/installments.md`). The correct path is most likely `https://docs.streampay.sa/docs/guides/installments.md`. The installment facts above come from the same guide as published in the developer-docs full export. - The Authentication guide says "HTTP Basic Authentication" but the actual mechanism is the **`x-api-key` header carrying the Base64 token**, not an `Authorization: Basic` header. Use `x-api-key`. - The Authentication guide's curl example posts `{"name","amount"}` to `/payment_links`; that is a placeholder. A real payment link requires `items` with `product_id`s, as in the Getting Started examples. - The Pagination guide's example hits `/api/v2/customers`; the real resource path is **`/api/v2/consumers`**. - The Branches guide example calls `/api/v2/user`; the identity endpoint listed in the API reference is "Get User And Organization Info" (`/api/v2/me`-style). Check the reference for the exact path. - The branch create path may differ between exports (`/api/v2/branch` vs the public reference's "Create Branch"). Confirm in the OpenAPI spec. - Sandbox vs production: the docs reference a sandbox environment and test cards but do not publish a separate sandbox base URL. Use the sandbox-mode key/dashboard Stream gives you, or ask support, rather than inventing a hostname. - Amounts in responses appear as **decimal strings in SAR** (e.g. `"2500.00"`). Compare as decimals, not floats. - When unsure about a field name, enum value, or required property, do not guess. Read `stream://docs/openapi` via the MCP server or the endpoint's page in the Table of Contents. ### Suggested agent workflow for "add Stream payments to my app" 1. Ask which flow: one-time, subscription, installments, or embedded/redirect. Confirm whether the cart ever mixes one-time and recurring items. 2. Create (or look up) the product(s), and the consumer if the customer is known. 3. Create the payment link from the backend; return only `url` to the client. 4. Implement the success/failure routes: read `invoice_id`, call `GET /invoices/{id}`, check `status`/`amount`/`currency`, then fulfill. 5. Implement the webhook endpoint with raw-body signature verification, idempotency, and fast 2xx. Subscribe to at minimum `PAYMENT_SUCCEEDED`, `PAYMENT_FAILED`, `INVOICE_COMPLETED`, plus `PAYMENT_PARTIALLY_REFUNDED` / `PAYMENT_REFUNDED` and the `SUBSCRIPTION_*` events if you sell subscriptions. 6. Test with the sandbox cards above (and Apple Pay amount ranges) before going live. --- ## Getting Started with Stream Welcome to **Stream**, your partner for seamless and secure online payments. This guide walks you through the essentials to integrate our **checkout experience** into your website or app. The full payment flow: > Your site → Stream Secure Checkout → Redirect to your success or failure page :::tip Using an AI assistant? Point it at [`/llms.txt`](pathname:///llms.txt) for an LLM-friendly index with integration instructions (full content in [`/llms-full.txt`](pathname:///llms-full.txt)). Any docs page is also available as Markdown by appending `.md` to its URL (for example `/webhooks.md`) or by requesting it with `Accept: text/markdown`. The [MCP server](/mcp-server) gives agents direct tool access. ::: ## Overview: Integration Steps 1. [Authorization](#authorization) 1. [Create a Customer (Consumer)](#create-a-customer-consumer) 1. [Creating a Payment Link](#creating-a-payment-link) 1. [Handling Payment Redirects](#handling-payment-redirects) 1. [Retrieve Payment or Subscription Status](#retrieve-payment-or-subscription-status) --- ## Authorization All API calls require authentication using your Stream API Key Pair (`api-key` + `api-secret`). 1. Navigate to Stream [API Settings](https://app.streampay.sa/settings/api-keys). 1. Click **Create Key Pair** and store the keys securely. To authenticate your requests, encode your API key and secret as a Base64-encoded token. Use the following command (replace `api-key` and `api-secret` with your actual values): ```bash echo -n 'api-key:api-secret' | base64 ``` Include this Base64-encoded token in the `x-api-key` authentication header for all API requests: ``` x-api-key: ``` --- ## Create a Customer (Consumer) Before you can generate a payment link for a specific customer, you must first create a customer (consumer) record in Stream. This step registers the customer in your organization and returns a unique identifier that you'll use to tie payment links to that customer. ### Why Create a Customer First? - **Personalized checkout**: When you attach a customer to a payment link, Stream skips the customer-details form during checkout because the customer's information is already known. - **Customer tracking**: Every invoice, subscription, and payment is linked back to the customer record, making reconciliation straightforward. - **Repeat billing**: For recurring products, the customer record is required to manage the subscription lifecycle. ### Step 1: Prepare the Customer Data Decide which contact fields to include. The only **required** field is `name`. All other fields are optional but recommended for a complete integration. | Field | Type | Required | Description | | ------------------------- | ---------- | -------- | --------------------------------------------------------------------------- | | `name` | string | **Yes** | Full name of the customer | | `phone_number` | string | No | Phone number (E.164 format, e.g., `+966501234567`) | | `email` | string | No | Email address | | `external_id` | string | No | Your own system's ID for this customer (useful for cross-referencing) | | `iban` | string | No | IBAN (max 34 characters) for bank-related operations | | `alias` | string | No | Short alias or display name | | `comment` | string | No | Internal notes about the customer | | `preferred_language` | string | No | Customer's preferred language for communications | | `communication_methods` | string[] | No | Preferred channels: `WHATSAPP`, `EMAIL`, `SMS` (can include multiple) | > **Tip**: Always include at least `phone_number` or `email` so Stream can send payment notifications to the customer. ### Step 2: Send the Create Request - **Endpoint**: `POST /api/v2/consumers` - **Auth**: `x-api-key: ` **cURL example:** ```bash curl -X POST https://stream-app-service.streampay.sa/api/v2/consumers \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "name": "Ahmad Ali", "email": "ahmad@example.com", "phone_number": "+966501234567", "external_id": "cust_12345", "communication_methods": ["EMAIL", "SMS"] }' ``` ### Step 3: Handle the Response A successful response (`200 OK`) returns the full customer record. The most important field is `id` — this is the `organization_consumer_id` you'll use when creating payment links. **Example response (simplified):** ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Ahmad Ali", "email": "ahmad@example.com", "phone_number": "+966501234567", "external_id": "cust_12345", "is_deleted": false, "created_at": "2025-06-15T10:30:00Z", "communication_methods": ["EMAIL", "SMS"], "preferred_language": "ar" } ``` > See the full response schema in the [Create Consumer API Reference](/api/v2-consumers-create). ### Step 4: Handle Errors If the request fails validation, the API returns a `422` status with an array of error details: ```json { "detail": [ { "loc": ["body", "name"], "msg": "Field required", "type": "missing" } ] } ``` Each object in the `detail` array contains `type` (error kind), `loc` (field path), and `msg` (human-readable message). Common error scenarios: | `type` / Scenario | `loc` | `msg` (example) | Resolution | | ------------------ | ----- | ---------------- | ---------- | | `missing` | `["body", "name"]` | `"Field required"` | Include the required `name` field in the request body | | `value_error` | `["body", "phone_number"]` | `"value is not a valid phone number"` | Use E.164 format: `+` (e.g., `+966501234567`) | | `value_error` | `["body", "email"]` | `"value is not a valid email address"` | Provide a properly formatted email address | The API may also return a non-validation error with a top-level error code instead of a `detail` array. The most common one: | Error Code | Cause | Resolution | | ---------- | ----- | ---------- | | `DUPLICATE_CONSUMER` | A consumer with the same phone number or email already exists | Use [Get All Consumers](/api/v2-consumers-list) to find the existing record and use its `id` instead | > **Tip**: Always check for an existing consumer before creating a new one to avoid `DUPLICATE_CONSUMER` errors. You can list consumers with `GET /api/v2/consumers` or match by `external_id` in your own system. ### Step 5: Use the Customer ID in a Payment Link Once you have the customer `id` from the response, pass it as `organization_consumer_id` when [creating a payment link](#creating-a-payment-link). This ties the payment to the specific customer and skips the customer-details step at checkout. > **Note**: If you omit `organization_consumer_id`, the payment link will collect customer details during checkout and create a new customer record automatically. Use this approach for general-purpose links shared with multiple customers. ### Managing Existing Customers After creating customers, you can manage them with these endpoints. The `{consumer_id}` path parameter is the `id` returned by `POST /api/v2/consumers` — the same value you pass as `organization_consumer_id` when creating payment links. | Action | Method | Endpoint | API Reference | | ---------- | -------- | ------------------------------------ | ------------------------------------- | | List all | `GET` | `/api/v2/consumers` | [Get All Consumers](/api/v2-consumers-list) | | Get one | `GET` | `/api/v2/consumers/{consumer_id}` | [Get Consumer](/api/v2-consumers-get) | | Delete | `DELETE` | `/api/v2/consumers/{consumer_id}` | [Delete Consumer](/api/v2-consumers-delete) | Before creating a new customer, you can check for an existing record by listing consumers or using your `external_id` to avoid duplicates. --- ## Creating a Payment Link Create a checkout/payment link using the [Payment Link API](/api/v2-payment-links-create). - **Endpoint**: `POST /api/v2/payment_links` - **Auth**: `x-api-key: ` Payment links redirect customers to Stream's secure checkout page. They support two use cases: 1. **For multiple customers**: Create a general-purpose link that anyone can use. Each customer enters their information during checkout. Perfect for events, group purchases, or public product sales. 1. **For one specific customer**: Create a link tied to a specific customer by setting `organization_consumer_id`. The checkout page will skip the customer details collection step since the customer information is already known. Use this when charging a known customer. When a customer pays through a payment link, the behavior depends on the product types you include: - **One-time products**: When the payment link is paid, Stream creates an invoice with the products. The payment can be split into multiple installments, but the product itself is a one-time purchase (not recurring). - **Recurring products**: When the payment link is paid, Stream creates a subscription (with recurring billing) and an initial invoice. The subscription will automatically generate invoices for future billing cycles based on the recurring interval. > **Important**: You cannot mix one-time and recurring products in the same payment link. All products must be either one-time or all must be recurring. ### Strategy: Single Order with Both One-Time + Recurring Items If a cart contains both one-time and recurring products, split it into **two payment links** and guide the customer through a short two-step checkout. This keeps each payment link valid while preserving a single “order” in your system. **General approach** - Create one link that contains only **one-time** items and a separate link that contains only **recurring** items. - Treat both links as part of the same internal order (e.g., by storing a shared order reference in metadata or your database). - Present the links sequentially (or based on your business preference) so the customer completes both steps. - Confirm both outcomes via your normal status checks or webhooks before marking the order as complete. > **Note**: You'll need existing products to create payment links. See the [Products API](/api/v2-products-create) for creating products. If you want to link a payment to a specific customer, you'll also need to create a customer first using the [Consumers API](/api/v2-consumers-create). ### Example: Payment Link for Multiple Customers This example creates a payment link that can be used by multiple different customers. Each customer will enter their information when they visit the checkout page. ```json { "name": "Event Registration", "description": "Annual conference registration", "items": [ { "product_id": "your-product-uuid-here", "quantity": 1 } ], "contact_information_type": "PHONE", "currency": "SAR", "max_number_of_payments": 100, "success_redirect_url": "https://example.com/success", "failure_redirect_url": "https://example.com/failure", "custom_metadata": { "event_id": "conf_2025", "campaign_source": "email_marketing" } } ``` **Code snippet (general-purpose, one-time product):** ```bash curl -X POST https://stream-app-service.streampay.sa/api/v2/payment_links \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "name": "Event Registration", "description": "Annual conference registration", "items": [ { "product_id": "your-product-uuid-here", "quantity": 1 } ], "contact_information_type": "PHONE", "currency": "SAR", "max_number_of_payments": 100, "success_redirect_url": "https://example.com/success", "failure_redirect_url": "https://example.com/failure" }' ``` > **Note**: `organization_consumer_id` is **not set**, so the link will collect customer information from each payer. ### Example: Payment Link for One Specific Customer This example creates a payment link for a specific customer. When you specify `organization_consumer_id`, the checkout page will skip the customer details collection step since the customer information is already known. ```json { "name": "Product Purchase", "description": "One-time payment for product order", "items": [ { "product_id": "your-product-uuid-here", "quantity": 1 } ], "contact_information_type": "PHONE", "currency": "SAR", "max_number_of_payments": 1, "organization_consumer_id": "your-customer-uuid-here", "success_redirect_url": "https://example.com/success", "failure_redirect_url": "https://example.com/failure", "custom_metadata": { "customer_id": "cust_12345", "order_id": "order_789" } } ``` > **Note**: `organization_consumer_id` **is set**, so the checkout page will skip the customer details collection step. The `max_number_of_payments` is typically set to `1` when targeting a specific customer. ### Example: Payment Link for a Recurring Product (New Subscription) Use this when you want a customer to start a subscription from checkout. The product itself must be **recurring** (e.g., monthly), which is configured when you create the product via the [Products API](/api/v2-products-create). The payment link request is **the same endpoint** as one-time links: - **Endpoint**: `POST /api/v2/payment_links` - **Auth**: `x-api-key: ` When the customer completes payment, Stream automatically creates a **subscription** and an **initial invoice**. Future invoices are generated based on the product's recurring interval. You can retrieve the created subscription via the [Subscriptions API](/api/v2-subscriptions-list). ```json { "name": "Premium Plan Subscription", "description": "Monthly recurring plan", "items": [ { "product_id": "recurring-product-uuid-here", "quantity": 1 } ], "contact_information_type": "EMAIL", "currency": "SAR", "max_number_of_payments": 1, "organization_consumer_id": "your-customer-uuid-here", "success_redirect_url": "https://example.com/success", "failure_redirect_url": "https://example.com/failure", "custom_metadata": { "plan": "premium_monthly", "source": "pricing_page" } } ``` **cURL example:** ```bash curl -X POST https://stream-app-service.streampay.sa/api/v2/payment_links \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "name": "Premium Plan Subscription", "description": "Monthly recurring plan", "items": [ { "product_id": "recurring-product-uuid-here", "quantity": 1 } ], "contact_information_type": "EMAIL", "currency": "SAR", "max_number_of_payments": 1, "organization_consumer_id": "your-customer-uuid-here", "success_redirect_url": "https://example.com/success", "failure_redirect_url": "https://example.com/failure" }' ``` > **Notes**: > - The recurring behavior is defined on the **product**, not the payment link. > - Do not mix one-time and recurring products in the same payment link. > - If `organization_consumer_id` is omitted, the checkout will collect customer details and the subscription will be created for that newly created customer. ### Response The API returns a payment link object. The most important field is `url`, which you'll redirect customers to: ```json { "url": "https://streampay.sa/s/...", "id": "payment-link-uuid", "status": "ACTIVE", "amount": "2500.00", "currency": "SAR" } ``` > **Note**: This response has been simplified for brevity. The full response includes additional fields like `items`, `custom_metadata`, `success_redirect_url`, and more. See the [API Reference](/api/v2-payment-links-create) for the complete response schema. Redirect your customer to the `url` field to complete the payment securely. ### Checkout Language The checkout page language can be overridden using the `language` query parameter on the payment link URL. By default, the checkout page is displayed to the customer in the language the organization is configured in. Example: ``` https://streampay.sa/s/...?...&language=en ``` Or: ``` https://streampay.sa/s/...?language=en ``` Supported values: | Value | Language | | ------- | ---------- | | `ar` | Arabic (default) | | `en` | English | > Note: This parameter only affects the hosted checkout page UI language. Data is not affected. --- ## Handling Payment Redirects After checkout, Stream can redirect the customer to your `success_redirect_url` or `failure_redirect_url` (if provided) with payment information as query parameters. If you omit these URLs, Stream will show its hosted success/failure pages instead. ### Redirect URL Examples **Success redirect:** ``` https://example.com/success?id=...&invoice_id=...&status=paid&message=APPROVED ``` **Failure redirect:** ``` https://example.com/failure?payment_link_id=...&id=...&status=failed&message=... ``` ### Query Parameters All redirects include these parameters: | Parameter | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `id` | Payment gateway payment ID - for internal use only. Useful for debugging when contacting support, but cannot be used in Stream APIs. Use `payment_id` or `invoice_id` instead. | | `payment_link_id` | UUID of the payment link that was used | | `status` | Payment status: `paid`, `pending`, `failed`, or `failed_internal_error` | | `message` | Status message from the gateway (e.g., `APPROVED`, `DECLINED`, `The customer’s bank has declined.`) | | `login_method` | Consumer's preferred login method: `PHONE` or `EMAIL` | :::info - **`status=failed`**: A definite rejection (e.g. insufficient funds, card declined). The payer should **retry** after addressing the issue. - **`status=failed_internal_error`**: Transient or unclear failures (e.g. timeouts). The payer should **contact support**; money may or may not have been taken. ::: **Additional parameters on success only:** | Parameter | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------ | | `invoice_id` | UUID of the invoice created from the payment link | | `consent_id` | UUID of the payment consent/authorization | | `payment_id` | UUID of the payment record in Stream's system | | `organization_consumer_id` | UUID of the customer who made the payment (newly created if not set when the payment link was created) | > **Note**: On failure, the additional parameters above are not included since the payment did not complete successfully. ### Processing the Redirect Parse the query parameters from the redirect URL on your server. :::danger **Important:** Always validate the payment server-side before accepting the order or completing any business action. Fetch the invoice using the `invoice_id` from the redirect with the [Invoice API](/api/v2-invoices-get), and verify its `status`, `amount`, and `currency` match your expectations. This server-side verification ensures the payment is legitimate before updating your system state. Never trust redirect parameters alone, always verify through our API. ::: --- ## Retrieve Payment or Subscription Status Use the **Get Payment** and **Get Subscription** endpoints to fetch the latest status programmatically. **Endpoints** - **Payment status:** `GET /api/v2/payments/{payment_id}` → [Payments API](/api/v2-payments-get) - **Subscription status:** `GET /api/v2/subscriptions/{subscription_id}` → [Subscriptions API](/api/v2-subscriptions-get) **Authentication** Use the same `x-api-key` header described in [Authorization](#authorization): ``` x-api-key: ``` ### Example: Fetch Payment Status (cURL) ```bash curl -X GET https://stream-app-service.streampay.sa/api/v2/payments/your-payment-id \ -H "x-api-key: " ``` ### Example: Fetch Subscription Status (cURL) ```bash curl -X GET https://stream-app-service.streampay.sa/api/v2/subscriptions/your-subscription-id \ -H "x-api-key: " ``` ### Example: Fetch Status in JavaScript (Node.js) ```js const API_BASE = "https://stream-app-service.streampay.sa/api/v2"; const API_KEY = process.env.STREAM_API_KEY_BASE64; // base64(api-key:api-secret) async function getPaymentStatus(paymentId) { const res = await fetch(`${API_BASE}/payments/${paymentId}`, { headers: { "x-api-key": API_KEY } }); if (!res.ok) throw new Error(`Payment fetch failed: ${res.status}`); const payment = await res.json(); return payment.status; } async function getSubscriptionStatus(subscriptionId) { const res = await fetch(`${API_BASE}/subscriptions/${subscriptionId}`, { headers: { "x-api-key": API_KEY } }); if (!res.ok) throw new Error(`Subscription fetch failed: ${res.status}`); const subscription = await res.json(); return subscription.status; } ``` > **Tip**: The `status` field in each response is the source of truth for the current state. See the full response schema in the linked API reference pages. --- ## Pagination & Filtering ## Pagination Pagination helps you efficiently navigate, sort, and filter large collections of data without fetching everything at once. This is especially useful for endpoints like: - [List Payments](/api/v2-payments-list) - [List Products](/api/v2-products-list) - [List Invoices](/api/v2-invoices-list) - [List Subscriptions](/api/v2-subscriptions-list) ## Filtering Filtering is another way to efficiently navigate and filter large collections of data without fetching everything at once. Whenever pagination is applied, then so is filtering. Filtering allows you to filter your fetched objects through specific features or conditions like queries. Whenever an endpoint may return many items, Stream uses a consistent pagination and filtering structure across all list responses. --- ## Request Parameters ### Pagination Parameters You can control pagination using the following query parameters: | Parameter | Type | Description | |------------------|--------|-----------------------------------------------------------------------------| | `page` | int | Page number starting from **1**. | | `limit` | int | Number of items per page. Maximum: **100**. | | `sort_field` | string | Field used to sort the results. | | `sort_direction` | string | Either `"asc"` or `"desc"` depending on sorting direction. | By default, `sort_field` is `created_at` and `sort_direction` is `desc`. ``` GET api/v2/products?page=1&limit=20&sort_field=created_at&sort_direction=desc ``` --- ### Filtering Parameters Filtering allows you to query objects based on specific attributes. When you apply filters, the `pagination` metadata in the response will reflect the total count of items *matching* your filter, not the total database count. | Parameter | Type | Description | |---------------|------------------|---------------------------------------------------------------------------------------------------| | `search_term` | `string` | A generic query string. The API searches relevant fields (e.g., names, IDs, descriptions) for matches. | | **Field Specific**| `string` / `array` | Specific attributes depending on the resource (e.g., `invoice_id`, `statuses`, `currency`). | **Note on Array Filters:** Some parameters, like `statuses`, accept multiple values. In the query string, this appears as repeating keys: `statuses=PENDING&statuses=PROCESSING` For example, the [List Payments](/api/v2-payments-list) endpoint takes filtering queries like `invoice_id` and `statuses` to filter down the returned payments. Example query: ``` GET api/v2/payments?page=1&limit=20&invoice_id=73b89d47-1bb1-443c-9a4c-f0ee23e22191&statuses=PENDING&statuses=PROCESSING ``` --- ## Pagination Response Structure Paginated responses include a `pagination` object that provides metadata about the current, previous, and next pages. | Field | Description | |--------------------|-----------------------------------------------------------------------------| | `total_count` | Number of objects returned in the current page. | | `max_page` | Maximum page number available. | | `current_page` | Page you are currently on. | | `limit` | Page size (echoes your request, capped at 100). | | `has_next_page` | Whether another page exists after this one. | | `has_previous_page`| Whether a page exists before this one. | Example response snippet: ``` { "data": [...], "pagination": { "total_count": 20, "max_page": 12, "current_page": 1, "limit": 20, "has_next_page": true, "has_previous_page": false } } ``` --- ## Examples (Python) Below are practical examples using Python's `requests` library. ### 1. Basic Pagination Fetching the first page of results with a custom limit. ```python API_KEY = "YOUR_API_KEY" BASE_URL = "https://stream-app-service.streampay.sa/api/v2/products" response = requests.get( BASE_URL, headers={"x-api-key": API_KEY}, params={ "page": 1, "limit": 20 } ) data = response.json() print(f"Showing page {data['pagination']['current_page']} of {data['pagination']['max_page']}") ``` ### 2. Global Search Filtering Using the `search_term` to find items matching a specific string (e.g., a customer name or partial ID). ```python API_KEY = "YOUR_API_KEY" BASE_URL = "https://stream-app-service.streampay.sa/api/v2/customers" response = requests.get( BASE_URL, headers={"x-api-key": API_KEY}, params={ "search_term": "JOHN DOE", # Searches for customer with that name "limit": 10 } ) results = response.json()["data"] print(f"Found {len(results)} matches for 'Al-Faisaliah'") ``` ### 3. Advanced Filtering (Lists & Specific Fields) Filtering by a specific `invoice_id` and multiple `statuses`. Note how the list of statuses is passed to the `params` dictionary. ```python API_KEY = "YOUR_API_KEY" BASE_URL = "https://stream-app-service.streampay.sa/api/v2/payments" # To filter by multiple statuses, pass them as a list payload_filters = { "page": 1, "limit": 50, "invoice_id": "73b89d47-1bb1-443c-9a4c-f0ee23e22191", "statuses": ["PENDING", "PROCESSING"] # Requests handles this as statuses=PENDING&statuses=PROCESSING } response = requests.get( BASE_URL, headers={"x-api-key": API_KEY}, params=payload_filters ) payments = response.json()["data"] for p in payments: print(f"Payment ID: {p['id']} | Status: {p['status']}") ``` ### 4. Iterating Through All Pages A robust pattern to fetch every item available. ```python API_KEY = "YOUR_API_KEY" BASE_URL = "https://stream-app-service.streampay.sa/api/v2/products" page = 1 all_products = [] while True: response = requests.get( BASE_URL, headers={"x-api-key": API_KEY}, params={"page": page, "limit": 100} # Maximize limit to reduce requests ) body = response.json() all_products.extend(body["data"]) # Stop if there are no more pages if not body["pagination"]["has_next_page"]: break page += 1 print(f"Successfully fetched {len(all_products)} products.") ``` --- --- ## Webhooks Webhooks allow your platform to receive real-time notifications from Stream when key events occur—such as payment successes, failures, or cancellations. This enables you to react to events programmatically, such as updating your database, sending notifications, or triggering internal workflows. Stream's webhook infrastructure is designed for reliability and includes features like: - Event-specific subscriptions - Secure payload signing - Delivery tracking for observability - Automatic retries using exponential backoff ## Creating a Webhook To create or manage your webhooks, go to [Webhook Settings](https://app.streampay.sa/settings/webhooks). There you can: - Register a new webhook endpoint - Select which event types you want to subscribe to - Enable, disable, or delete webhooks ## Webhook Flow Logic Understanding which webhooks are sent in different scenarios: ### Single Payment Succeeds When a single payment succeeds: 1. `PAYMENT_SUCCEEDED` webhook is sent (one per payment) 2. If the payment completes the invoice (all payments are now paid), `INVOICE_COMPLETED` webhook is also sent Example: Invoice with 1 payment of 1000 SAR - Payment succeeds → `PAYMENT_SUCCEEDED` webhook - Invoice completes → `INVOICE_COMPLETED` webhook ### Multiple Payments Succeed (Installments) When multiple payments succeed together (e.g., installments): 1. `PAYMENT_SUCCEEDED` webhook is sent for each payment that succeeded (one per payment) 2. If all payments complete the invoice, `INVOICE_COMPLETED` webhook is also sent Example: Invoice with 3 payments of 1000 SAR each - Payments 1, 2, 3 all succeed together: - `PAYMENT_SUCCEEDED` webhook for payment_1 - `PAYMENT_SUCCEEDED` webhook for payment_2 - `PAYMENT_SUCCEEDED` webhook for payment_3 - `INVOICE_COMPLETED` webhook Example: Invoice with 5 payments, payments 1-3 succeed together: - `PAYMENT_SUCCEEDED` webhook for payment_1 - `PAYMENT_SUCCEEDED` webhook for payment_2 - `PAYMENT_SUCCEEDED` webhook for payment_3 - No `INVOICE_COMPLETED` webhook (invoice not yet fully paid) ### Payment Fails When a payment fails: 1. `PAYMENT_FAILED` webhook is sent 2. No invoice completion webhook is sent ### Payment Refunded (Partial or Full) Stream supports multiple refunds on a single payment. Webhook events depend on whether the payment is fully refunded or only partially refunded: **Partial refund** (cumulative refunded amount is still less than the payment amount): 1. `PAYMENT_PARTIALLY_REFUNDED` webhook is sent 2. Payment status becomes `PARTIALLY_REFUNDED` 3. Invoice status does not change **Full refund** (cumulative refunded amount equals the payment amount): 1. `PAYMENT_REFUNDED` webhook is sent 2. Payment status becomes `REFUNDED` 3. Invoice status does not change Example: Payment of 1000 SAR - First refund of 300 SAR → `PAYMENT_PARTIALLY_REFUNDED` (`amount_refunded` = 300, `total_amount_refunded` = 300) - Second refund of 200 SAR → `PAYMENT_PARTIALLY_REFUNDED` (`amount_refunded` = 200, `total_amount_refunded` = 500) - Final refund of 500 SAR → `PAYMENT_REFUNDED` (`amount_refunded` = 500, `total_amount_refunded` = 1000) :::info `PAYMENT_REFUNDED` means the payment is **fully** refunded. If you care about partial refunds, also subscribe to `PAYMENT_PARTIALLY_REFUNDED`. ::: ### Payment Marked as Paid When a payment is manually marked as paid: 1. `PAYMENT_MARKED_AS_PAID` webhook is sent 2. If this completes the invoice, `INVOICE_COMPLETED` webhook is also sent ### Subscription Renewal When a subscription cycle renews: 1. New invoice created → `INVOICE_CREATED` webhook 2. Payment succeeds → `PAYMENT_SUCCEEDED` webhook 3. Invoice completes → `INVOICE_COMPLETED` webhook **Note:** For subscription renewals, the `INVOICE_COMPLETED` webhook indicates that the subscription cycle has been renewed successfully. The invoice will have a `subscription_id` in its payload, and you can check if it's a renewal invoice by comparing it with previous invoices for that subscription. ## Event Types ### Payment Events - `PAYMENT_SUCCEEDED` - Payment completed successfully - `PAYMENT_FAILED` - Payment attempt failed - `PAYMENT_CANCELED` - Payment was canceled - `PAYMENT_PARTIALLY_REFUNDED` - A refund was issued and the payment is still only partially refunded - `PAYMENT_REFUNDED` - Payment was fully refunded - `PAYMENT_MARKED_AS_PAID` - Payment was manually marked as paid ### Invoice Events - `INVOICE_CREATED` - New invoice created - `INVOICE_SENT` - Invoice sent to consumer - `INVOICE_ACCEPTED` - Invoice accepted by consumer - `INVOICE_REJECTED` - Invoice rejected by consumer - `INVOICE_COMPLETED` - Invoice completed (all payments done) - `INVOICE_CANCELED` - Invoice canceled - `INVOICE_UPDATED` - Invoice details updated ### Subscription Events - `SUBSCRIPTION_CREATED` - New subscription created - `SUBSCRIPTION_ACTIVATED` - Subscription changed to `ACTIVE` (initial activation or reactivation) - `SUBSCRIPTION_INACTIVATED` - Subscription deactivated - `SUBSCRIPTION_CANCELED` - Subscription canceled - `SUBSCRIPTION_FROZEN` - Subscription frozen - `SUBSCRIPTION_CYCLE_RENEWED_SUCCESSFULLY` - Renewal payment succeeded and the subscription was renewed - `SUBSCRIPTION_CYCLE_RENEWAL_FAILED` - Automated/tokenized renewal payment failed - `SUBSCRIPTION_CANCEL_AT_PERIOD_END` - Subscription scheduled to cancel at period end - `SUBSCRIPTION_FREEZE_NOW` - Subscription frozen immediately - `SUBSCRIPTION_UNFREEZE_NOW` - Subscription unfrozen immediately - `SUBSCRIPTION_UNFREEZE_FUTURE` - Subscription scheduled to unfreeze in future - `SUBSCRIPTION_FREEZE_CANCEL` - Subscription freeze canceled - `SUBSCRIPTION_PLAN_CHANGE_SCHEDULED` - A plan change was scheduled to take effect at `current_period_end`. - `SUBSCRIPTION_PLAN_CHANGE_CANCELED` - A previously scheduled plan change was canceled before it took effect. - `SUBSCRIPTION_PLAN_CHANGED` - The scheduled plan change was actually applied — items, coupons, interval, and amount are now live on the subscription. - `SUBSCRIPTION_PLAN_CHANGE_INVOICE_REISSUED` - While scheduling a plan change, an existing prepared next-cycle invoice was canceled and regenerated from the new target plan. This is triggered alongside `SUBSCRIPTION_PLAN_CHANGE_SCHEDULED` if it affects the plan's invoice. - `SUBSCRIPTION_PLAN_UPDATED` - Subscription plan change was applied immediately. This applies for changing seats, adding add-ons, and changing number of subscription cycles. ### Payment Link Events - `PAYMENT_LINK_PAY_ATTEMPT_FAILED` - Payment attempt failed for a payment link **Note:** This webhook is sent when a payment link payment fails. Unlike regular payment failures, no invoice, payment, or subscription entities are created on failure, so `PAYMENT_FAILED` webhook cannot be sent. This event exists specifically to notify about payment link failures when no entities exist. ## Webhook Payload The webhook POST request will have a JSON body like: ```json { "event_type": "PAYMENT_SUCCEEDED", "entity_type": "PAYMENT", "entity_id": "uuid", "entity_url": "", "status": "SUCCEEDED", "data": { /* payment or event-specific data */ }, "timestamp": "2025-07-15T14:41:21.705Z" } ``` ## Webhook Payload Example ```json { "data": { "invoice": { "id": "2df0f7e0-2634-46ab-829a-bfcb0a797d87", "url": "https://stream-app-service.streampay.sa/api/v2/invoices/2df0f7e0-2634-46ab-829a-bfcb0a797d87" }, "payment": { "id": "e2182d3d-b4cf-4972-bcc0-ec6d963c066d", "url": "https://stream-app-service.streampay.sa/api/v2/payments/e2182d3d-b4cf-4972-bcc0-ec6d963c066d" }, "payment_link": { "id": "6361941e-3a81-4aa5-aaa6-60d6e052883a", "url": "https://stream-app-service.streampay.sa/api/v2/payment_links/6361941e-3a81-4aa5-aaa6-60d6e052883a" }, "metadata": { "customer_id": "cust_12345", "nested_data": { "sub_field": "sub_value", "sub_number": 678 }, "affiliate_id": "aff_987", "custom_field_1": "value_1", "custom_field_2": 12345, "campaign_source": "email_marketing" } }, "status": "SUCCEEDED", "entity_id": "e2182d3d-b4cf-4972-bcc0-ec6d963c066d", "timestamp": "2025-07-22T14:40:31.485576", "entity_url": "https://stream-app-service.streampay.sa/api/v2/payments/e2182d3d-b4cf-4972-bcc0-ec6d963c066d", "event_type": "PAYMENT_SUCCEEDED", "entity_type": "PAYMENT" } ``` ## Webhook Request Headers - `Content-Type: application/json` - `User-Agent: StreamApp-Webhook/1.0` - `X-Webhook-Event`: Event type (e.g., `PAYMENT_SUCCEEDED`) - `X-Webhook-Entity-Type`: Entity type (e.g., `PAYMENT`) - `X-Webhook-Entity-ID`: Entity UUID - `X-Webhook-Signature`: Format: `t={timestamp},v1={signature}` - `timestamp` is the UNIX timestamp when the signature was generated. - `signature` is an HMAC-SHA256 of `{timestamp}.{payload}` using your webhook’s `secret_key`. - `X-Webhook-Timestamp`: The timestamp used in the signature. ## Verifying Webhook Authenticity :::warning Always verify the `X-Webhook-Signature` before treating a webhook as authentic or updating your system (fulfillment, access, financial state). Reject requests with a missing or invalid signature. Unsigned or unverified payloads could be forged; never trust the JSON body alone. ::: To verify a webhook: 1. Extract the `X-Webhook-Signature` header and split into timestamp and signature. 2. Compute the HMAC-SHA256 of `{timestamp}.{raw_request_body}` using your webhook’s `secret_key`. 3. Compare your computed signature to the value in the header. Example (Python): ```python def verify_webhook_signature(secret: str, raw_body: bytes, signature_header: str): # signature_header: "t=TIMESTAMP,v1=SIGNATURE" parts = dict(x.split('=') for x in signature_header.split(',')) timestamp = parts['t'] signature = parts['v1'] message = f"{timestamp}.{raw_body.decode()}" computed = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(computed, signature) ``` ## Webhook Delivery & Retry Logic - Delivery Attempts: Every webhook event triggers a delivery record. Each delivery attempt and its status (pending, delivered, failed, retrying) is tracked for auditing. - Retry Schedule: If your endpoint does not respond with a 2xx status, Stream will retry delivery up to 5 times. The default retry delays (in minutes) are: 1. 1st retry: 5 minutes after the first failure 2. 2nd retry: 30 minutes after the second failure 3. 3rd retry: 2 hours (120 minutes) after the third failure 4. 4th retry: 6 hours (360 minutes) after the fourth failure 5. 5th retry: 12 hours (720 minutes) after the fifth failure After the fifth attempt, if delivery is still unsuccessful, the webhook delivery is marked as failed and no further attempts will be made. - Auditing: All delivery attempts, responses, and errors are logged and can be audited. --- ## Testing Cards ## Testing Environment In order to test your application without having to go live with cards and accounts, you can use [Moyasar's testing cards](https://docs.moyasar.com/guides/card-payments/test-cards/)) to simulate different transaction outcomes. The test cards should be used in a sandbox environment. This is also applicable if you are using the Stream's UI in sandbox mode and want to test the flow without having to use actual cards. ## Required Card Fields Below are the values that need to be entered for the non-card number fields when testing: | Field | Value | | :--- | :--- | | **Name** | Any name made of two words separated by a space (e.g., John Doe) | | **Year** | Any **future** year | | **Month** | Any month (or a future month if using the current year) | | **CVC** | Any 3 digits (4 digits for American Express (AMEX)) | --- ## List of Test Cards and Expected Outcomes Below is the list of test card numbers you can use in your test environment to simulate specific transaction statuses (Success/Failure) according to the card network. ### Mada | Card Number | Status | Message | Response Code | 3DS Note | | :--- | :--- | :--- | :--- | :--- | | `4201320111111010` | **paid** | APPROVED | 00 | ---- | | `4201320000013020` | **failed** | UNSPECIFIED FAILURE | 99 | ---- | | `4201320000311101` | **failed** | INSUFFICIENT FUNDS | 51 | ---- | | `4201320131000508` | **failed** | DECLINED: LOST CARD | 41 | ---- | | `4201321234411220` | **failed** | DECLINED | 05 | ---- | | `4201322267774310` | **failed** | DECLINED: EXPIRED CARD | 54 | ---- | | `4201326324640570` | **failed** | DECLINED: EXCEEDS WITHDRAWAL LIMIT | 61 | ---- | | `4201321144311528` | **failed** | DECLINED: STOLEN CARD | 43 | ---- | ### Visa | Card Number | Status | Message | Response Code | 3DS Note | | :--- | :--- | :--- | :--- | :--- | | `4111114005765430` | **paid** | APPROVED | 00 | Frictionless Authentication | | `4111111111111111` | **paid** | APPROVED | 00 | ---- | | `4123120000000000` | **failed** | UNSPECIFIED FAILURE | 99 | ---- | | `4123120001090000` | **failed** | INSUFFICIENT FUNDS | 51 | ---- | | `4123450131000508` | **failed** | DECLINED: LOST CARD | 41 | ---- | | `4123120001090109` | **failed** | DECLINED | 05 | ---- | | `4123128518640738` | **failed** | DECLINED: EXPIRED CARD | 54 | ---- | | `4123123033308648` | **failed** | DECLINED: EXCEEDS WITHDRAWAL LIMIT | 61 | ---- | | `4123125276780003` | **failed** | DECLINED: STOLEN CARD | 43 | ---- | | `4111118250252531` | **failed** | 3DS: attempted but not available, please ensure that you have enabled Online Purchase from your bank portal. | ---- | ECI 06 | | `4111113343111067` | **failed** | 3DS service error occurred. | ---- | 3DS fails during enrollement check | | `4111116611600661` | **failed** | The card is not enrolled in 3DS service. | ---- | ---- | | `4111112205628150` | **failed** | 3DS service error occurred. | ---- | 3DS fails during authentication attempt | | `4111115784228433` | **failed** | The authentication attempt was rejected by the issuer bank. | ---- | ---- | | `4111115620358287` | **failed** | The authentication is unavailable, please try again later or contact issuer bank if problem persisted. | ---- | ---- | ### Mastercard | Card Number | Status | Message | Response Code | 3DS Note | | :--- | :--- | :--- | :--- | :--- | | `5421080101000000` | **paid** | APPROVED | 00 | ---- | | `5105105105105100` | **failed** | UNSPECIFIED FAILURE | 99 | ---- | | `5457210001000092` | **failed** | INSUFFICIENT FUNDS | 51 | ---- | | `5204010101000000` | **failed** | DECLINED: LOST CARD | 41 | ---- | | `5204730000002514` | **failed** | DECLINED | 05 | ---- | | `5105107550274126` | **failed** | DECLINED: EXPIRED CARD | 54 | ---- | | `5105106475101067` | **failed** | DECLINED: EXCEEDS WITHDRAWAL LIMIT | 61 | ---- | | `5105107304607225` | **failed** | DECLINED: STOLEN CARD | 43 | ---- | ### American Express (AMEX) | Card Number | Status | Message | Response Code | 3DS Note | | :--- | :--- | :--- | :--- | :--- | | `340000000900000` | **paid** | APPROVED | 00 | ---- | | `371111111111114` | **failed** | UNSPECIFIED FAILURE | 99 | ---- | | `340033000000000` | **failed** | INSUFFICIENT FUNDS | 51 | ---- | | `340012340501000` | **failed** | DECLINED: LOST CARD | 41 | ---- | | `340033000000133` | **failed** | DECLINED | 05 | ---- | | `340000018441278` | **failed** | DECLINED: EXPIRED CARD | 54 | ---- | | `340000753060788` | **failed** | DECLINED: EXCEEDS WITHDRAWAL LIMIT | 61 | ---- | | `340000418501838` | **failed** | DECLINED: STOLEN CARD | 43 | ---- | --- ## Testing Apple Pay Unlike card payments where specific test card numbers are used, testing **Apple Pay** payments in the Stream's sandbox environment is based on the **amount** sent to the API (according to [Moyassar](https://docs.moyasar.com/guides/apple-pay/testing/)). A **real card** must be added to an Apple Pay Wallet to test the process, but you must be using the sandbox environment to ensure no actual payment is made. The following amounts will simulate different payment outcomes: | Amount (Minor Unit) | Amount (SAR) | Status | Message | Response Code | | :--- | :--- | :--- | :--- | :--- | | `20000` to `30000` | `200.00` to `300.00` | **paid** | APPROVED | 00 | | `100000` to `110000` | `1000.00` to `1100.00` | **failed** | UNSPECIFIED FAILURE | 99 | | `110100` to `120000` | `1101.00` to `1200.00` | **failed** | INSUFFICIENT FUNDS | 51 | | `120100` to `130000` | `1201.00` to `1300.00` | **failed** | DECLINED: LOST CARD | 41 | | `130100` to `140000` | `1301.00` to `1400.00` | **failed** | DECLINED | 05 | | `140100` to `150000` | `1401.00` to `1500.00` | **failed** | DECLINED: EXPIRED CARD | 54 | | `150100` to `160000` | `1501.00` to `1600.00` | **failed** | DECLINED: EXCEEDS WITHDRAWAL LIMIT | 61 | | `160100` to `170000` | `1601.00` to `1700.00` | **failed** | DECLINED: STOLEN CARD | 43 | > **Note:** Moyasar accepts the payment amount in the **minor currency unit** (e.g., cents), and the test environment uses this amount to return different results. --- > **Tip:** Using any other amount range than the stated above will result in the payment failing. --- ## Installments Installments allow customers to split a payment into multiple smaller payments over time. Customers can choose from available installment options (2, 3, or 4 installments) when paying an invoice or payment link. > Note: Installments are only available for one-time products. They are automatically disabled for recurring products (subscriptions). --- ## How Installments Work When a customer selects an installment option, Stream automatically calculates the amount for each installment and schedules them based on the split period. Each installment becomes a separate payment with its own due date. ![Customer selecting installment split option](/img/installment-split-selection.png) **Example:** 1,000 SAR split into 3 monthly installments: - Installment 1: 333.33 SAR (due on payment date) - Installment 2: 333.33 SAR (due 1 month later) - Installment 3: 333.34 SAR (due 2 months later) --- ## How Installment Amounts Are Calculated Installment amounts are calculated by dividing the total payment by the number of installments. If the amount doesn't divide evenly, any rounding difference is added to the last installment to ensure the total always equals the original payment amount. For example, 999.98 SAR split into 3 installments results in: 333.33 SAR, 333.33 SAR, and 333.32 SAR (the last installment is reduced by 0.01 SAR to account for the rounding difference). > Note: The total of all installments always equals the original payment amount. --- ## Configuration Configure installment settings in your [Payment Settings](https://app.streampay.sa/settings/payment-settings). Invoice and payment link settings are configured separately. ### Enable/Disable Installments #### API (Payment Links) When creating a payment link via the [Payment Links API](/api/v2-payment-links-create), you can enable or disable installments by including the `payment_methods` parameter in your request. Set `installment: true` to enable installments or `installment: false` to disable them for that specific payment link. This setting overrides the organization's default payment link installment settings. If you don't include the `payment_methods` parameter, the organization's default settings will be used. #### Settings Page You can configure the default installment settings in your [Payment Settings](https://app.streampay.sa/settings/payment-settings): - **For invoices**: Enable or disable installments in the invoice payment options (invoices can only be created from the UI) - **For payment links**: Enable or disable installments in the payment link payment options (these defaults apply when creating payment links via the UI or when the API request doesn't specify `payment_methods`) ### Split Options Configure which numbers of installments (2, 3, or 4) are available for customers to choose from. Default split options: - 2 installments, monthly - 3 installments, monthly - 4 installments, monthly The split period (the interval between installments, e.g., monthly, quarterly, yearly) cannot be changed through the API or dashboard. Contact Stream support to modify the split period. ### Minimum Amount The minimum installment amount is 100 SAR per installment. If the payment amount divided by the number of installments is less than 100 SAR, that installment option will not be available. For example, with a payment of 250 SAR: - 2 installments: 125 SAR each ✅ (available) - 3 installments: 83.33 SAR each ❌ (not available, below minimum) The minimum amount cannot be changed through the API or dashboard. Contact Stream support to modify this value. --- ## Authentication Learn how to authenticate your API requests to the Stream platform. ## Overview All API calls to Stream require authentication using your API Key Pair. Stream uses a simple but secure authentication method where you encode your credentials and include them in every request. ## Getting Your API Keys 1. Log in to your Stream dashboard 2. Navigate to [API Settings](https://app.streampay.sa/settings/api-keys) 3. Click **Create Key Pair** 4. Store your `api-key` and `api-secret` securely 5. Copy `x-api-key` to use in the APIs or you can gernerate later :::warning Important Your API secret should be kept secure and never exposed in client-side code or public repositories. Treat it like a password. ::: ## Authentication Methods Stream uses HTTP Basic Authentication with Base64 encoding of your API credentials. Encode your API key and secret as a Base64-encoded token: ```bash echo -n 'api-key:api-secret' | base64 ``` **Example:** ```bash echo -n '038cb769-9ce3-49da-9d52-17fd49bf07e8:6ea68e79-8756-4be8-9bd2-307314e8d12c' | base64 # Output: MDM4Y2I3NjktOWNlMy00OWRhLTlkNTItMTdmZDQ5YmYwN2U4OjZlYTY4ZTc5LTg3NTYtNGJlOC05YmQyLTMwNzMxNGU4ZDEyYw== ``` #### Using the Token Include the Base64-encoded token in the `x-api-key` header for all API requests: ```bash curl -X POST https://stream-app-service.streampay.sa/api/v2/payment_links \ -H "x-api-key: MDM4Y2I3NjktOWNlMy00OWRhLTlkNTItMTdmZDQ5YmYwN2U4OjZlYTY4ZTc5LTg3NTYtNGJlOC05YmQyLTMwNzMxNGU4ZDEyYw==" \ -H "Content-Type: application/json" \ -d '{ "name": "Test Payment", "amount": 100.00 }' ``` ### Using SDKs When using Stream SDKs, authentication is handled automatically. Simply provide your `x-api-key` during initialization: #### TypeScript SDK ```typescript const client = StreamSDK.init(process.env.STREAM_API_KEY!); ``` #### Express SDK ```typescript app.get( "/checkout", Checkout({ apiKey: process.env.STREAM_API_KEY!, // ... other config }) ); ``` --- ## Branches in the API API requests are always scoped to a single branch. When creating an API key, you specify which branches it can access. For each request, you can select the target branch, which determines where resources are created and accessed. ## Creating API Keys with Branch Access When creating an API key, you can specify: - Branches: List of specific branches the API key can access - Default Branch: Default branch used when `x-branch-id` header is not provided - Is All Branches: If `true`, the API key has access to all branches, present and future ## Selecting a Branch in API Requests Include the `x-branch-id` header to specify which branch to use for that request: ```bash curl -X GET https://stream-app-service.streampay.sa/api/v2/user \ -H "x-api-key: YOUR_API_KEY" \ -H "x-branch-id: branch-uuid-here" ``` If `x-branch-id` is omitted, the API key's `default_branch_id` is used. ## Branch Access Validation The API key must have access to the branch specified in `x-branch-id`: - If `is_all_branches=true`: Access to all organization branches - Otherwise: Only branches listed in `branch_ids` Requests to unauthorized branches return a 4xx error. --- ## Customer Portal (Hosted Billing) # Customer Portal (hosted billing) The **customer portal** is Stream’s hosted web app ([billing.streampay.sa](https://billing.streampay.sa)) where your **consumers** sign in and manage their relationship with **you**, within limits you configure in Stream: subscriptions (plan changes, quantity adjustment, and add‑ons allowed by your catalog), invoices, saved cards, profile and language, and payment flows that use Stream checkout. --- ## Prerequisites - **Consumer in Stream** — The person must exist as an **organization consumer** (Stream’s customer record). Create one with the [Create Consumer](/api/v2-consumers-create) API (or your existing consumer flow) and use the returned **`id`** as **`organization_consumer_id`** when creating a portal session. - **Merchant API credentials** — Your backend calls Stream with **`x-api-key`**; see [Authentication](/authentication). --- ## Send a consumer to the portal Call from your **server** only (never expose your API secret or full key pair in a browser or app) this [endpoint](/api/v2-consumer-portal-sessions-create) to get the one-time use token to open the customer portal for a specific consumer ID. **Body (minimum)** | Field | Required | Description | | ----- | -------- | ----------- | | `organization_consumer_id` | Yes | The consumer’s UUID in Stream (`id` from [Create Consumer](/api/v2-consumers-create) or [list consumers](/api/v2-consumers-list)). | **Response** On success (**HTTP 201**) the JSON includes a **`url`**. Return that **`url`** to your frontend and open it so the consumer lands on the billing portal. You can optionally include **`return_url`** (query hint for “back to merchant”; Stream does not auto-redirect), **`subscription_id`** to open a specific subscription for that consumer, or other optional fields supported by the API—see the reference for the full request schema. --- ## What subscription self-service means When your settings allow it, a customer can change an **active** subscription from the portal. Stream validates every change against your catalog and portal rules. Below is what each kind of change means in plain terms. ### Switching plans **Plan switching** is when the customer moves from one **base** subscription product to another **member of the same product switch group** you defined in Stream. Only products you placed in that group can replace one another; the portal does not allow arbitrary jumps across unrelated products. Plans with different recurring cycles (e.g MONTHLY and YEARLY) can be added in the same switch group. Typical shape of a switch: one billed **base** line is removed and another **base** line (the new plan) replaces it within the rules of that group. Whether this appears as an immediate change or is scheduled depends on billing setup and proration logic (see [Proration and timing](#proration-and-timing)). :::note Coupons Subscriptions with **coupon** locked at subscription level usually **cannot** be edited from the portal. Coupons on lines you keep can persist; lines you remove lose their coupons. ::: ### Add-ons **Add-ons** are separate catalog products attached to an active subscription—not the main plan by itself. Each add-on is only available when the subscription’s **base** catalog products make it eligible. You declare that relationship in Stream (which bases may carry which add-ons). Customers can subscribe or drop add-ons in the portal only when you enable **add / remove add-ons** for Customer Portal and the add-on mapping allows it. When creating an add-on, you get to choose a list of recurring products for which the add-on is eligible. In case a subscription has multiple recurring products and the recurring products have different eligible add-ons, the customer will be able to add **all** of these add-ons. ### Changing quantity Quantity changes behave differently depending on whether the line is part of your **switch group** (often “seat-like” bases) versus an **add-on**: | Kind of line | What “quantity” means | How you control it | | ------------ | --------------------- | ------------------ | | **Products in switch groups** | How many seats or units apply to that **base plan** row | Toggle **switch-group quantity change** in Customer Portal settings. When enabled, you can constrain **minimum** and **maximum** quantities; when disabled, customers cannot freely change those quantities in the portal. | | **Add-on lines** | How many units of that add-on are on the subscription | Toggle **add-on quantity change**. When quantity change is **off** for new add-ons, new add-ons are typically limited to quantity **one** until you widen the rule. | The portal compares changes to **allowed** quantities only; anything outside configured bounds is rejected. ### Proration and timing {#proration-and-timing} **Proration** is how billing adjusts when the subscription **mid-cycle** gains or loses value: Stream compares what was prepaid or owed for the current period against what the revised subscription implies, and settles the difference—often creating an invoice line or prompting payment **before** the next renewal. Proration does not include the current day in the new pricing. For example, if a monthly subscription (30 days) was created today and then in the same day an add-on gets added immediately through proration, the proration excludes the current day from the new bill (i.e. the user pays 29 / 30 days worth of add-on instead of 30/30.) Roughly: - **Adding add-ons**, **raising quantity**, or some **plan upgrades** commonly produce **immediate** charges (or payment steps) when money is owed, because billing value rises right away relative to what was invoiced. - **Removing add-ons**, **lowering quantity**, or changing plans in directions that reduce or reshape future billing often take effect according to billing rules. For example at **period end** or as a **scheduled** change rather than forcing an instant upfront payment in every case. - When a change crosses into a **different billing rhythm** than the subscription’s targets, moves are **often scheduled** for a future boundary rather than squeezed into an immediate payment-only flow inside the portal. From the customer’s perspective, the portal either **confirms the change outright**, **shows a payment or card step** when an adjustment is due now, or **explains that the change applies later** in the billing period—depending on the scenario above. Until any required payment clears, billing may remain on the previous shape until reconciliation finishes. Exact math and timing depend on your products’ recurrence and Stream’s reconciliation; you do **not** re-implement proration in your integration. --- ## Configure Customer Portal, branding, and catalog Open the Stream dashboard, then **[Settings → Customer Portal](https://app.streampay.sa/settings/customer-portal)**. There you tune both **permissions** customers get and **branding** of [billing.streampay.sa](https://billing.streampay.sa). ### Portal permissions (toggles) These settings mirror what you enable in Customer Portal Settings in the UI—they define what edits the hosted app even offers: | Setting | Meaning for your customers | | ------- | -------------------------- | | **Product switch** | Can move between products you grouped in **product switch groups** (plan changes within your allowed set). | | **Add / remove add-ons** | Can attach or remove add-ons you exposed through add-on eligibility. | | **Switch-group quantity change** | Can change quantity on **base** lines that belong to switch groups, within optional **min** / **max** when enabled. | | **Add-on quantity change** | Can change quantity on add-on lines, within optional **min** / **max** when enabled. | | **Subscription cancellation** | Customer-led cancellation inside portal flows is on or off. | ### Branding On the same **Customer Portal** settings area, you can personalize the hosted experience: - **Primary color** and **secondary color** — Applied across the portal UI so it feels closer to your brand. - **Favicon** — Shown in the browser tab when customers use the billing site. Uploads and color pickers persist with your organization so every session link inherits the same look. ### Catalog prerequisites (beyond the toggles) Customer Portal switches and add-ons only work if the **catalog** is set up to match: | Goal | What to set up in Stream | | ---- | ------------------------- | | Allowed plan moves | Create **product switch groups** and assign which products belong to each group. Only members of the **same** group can replace one another. | | Which add-ons exist for which bases | Maintain **add-on mappings**: for each add-on product, declare which **base** products make it eligible. | Until those mappings exist, toggles alone will not expose meaningful switches or add-ons in the portal. --- ## Summary | Role | Responsibility | | ---- | ---------------- | | **Your backend** | `POST /api/v2/consumer_portal/sessions` with `organization_consumer_id`, then return **`url`** to the client. | | **Your frontend** | Open **`url`**; do not use the merchant API key in the browser. | | **billing.streampay.sa** | Hosts the customer portal once the consumer opens **`url`. | For API key setup and authentication, see [Authentication](/authentication). --- ## Embedded Checkout Render a payment link checkout in-page with the Stream Embed SDK. Create a payment link via the API first, then pass the response **`url`** into `paymentLink`. Details on creating links (products, consumers, redirects): [Creating a Payment Link](/#creating-a-payment-link). ## Prerequisites - Same setup as payment-link creation: API key pair, and at least one product ID. See [Creating a Payment Link](/#creating-a-payment-link). - For using Apple Pay through our embedded checkouts, the following steps need to be done: - Get the text file from `https://streampay.sa/.well-known/apple-developer-merchantid-domain-association` - Host the same exact text file under your domain through this path: `.well-known/apple-developer-merchantid-domain-association` - Reach out to support team on available communication methods to register you in the PGW system. ## Example ### 1. Create a payment link `POST /api/v2/payment_links` returns an object whose **`url`** field is what you embed (same value you would redirect to). ```bash curl -s -X POST https://stream-app-service.streampay.sa/api/v2/payment_links \ -H "x-api-key: " \ -H "Content-Type: application/json" \ -d '{ "name": "Checkout", "items": [{ "product_id": "your-product-uuid-here", "quantity": 1 }], "contact_information_type": "PHONE", "currency": "SAR", "max_number_of_payments": 100 }' ``` Example response (trimmed): ```json { "url": "https://streampay.sa/ds/..." } ``` Use that `url` as `paymentLink` in the next step. Build the request body you need (customer-specific links, metadata, redirect URLs, subscriptions, etc.) using the guide and [Create payment link](https://docs.streampay.sa/api/v2-payment-links-create). ### 2. Embed checkout Load the default SDK, mount a container, call `Stream.Checkout` with the **`url`** from step 1. Inject the URL from your server—do not create payment links or expose API keys in frontend-only code. ```html ``` ## Options | Option | Type | Required | Description | | ------ | ---- | -------- | ----------- | | `paymentLink` | `string` | **Yes** | Full payment-link URL from the API (`url` field) or short ID Stream accepts | | `container` | `string` \| `Element` | **Yes** | CSS selector or DOM element for the iframe | | `minHeightPx` | `number` | No | Min iframe height (default **280**) | | `maxHeightPx` | `number` | No | Max iframe height (default **920**) | | `vhCapPx` | `number` | No | Viewport-relative height cap (default **800**) | | `vhFraction` | `number` | No | Viewport fraction for fluid height (default **0.7**) | ## See also - [Webhooks](/webhooks) — confirm payment server-side - [Public API reference](https://docs.streampay.sa/) --- ## Stream API Errors This page documents the error response format and the standardized error codes returned by Stream API endpoints. The goal is to provide a stable, predictable contract for client applications and integrators. ## Error response envelope All handled errors return a response body using the `StreamBaseResponse` envelope: ```json { "error": { "code": "STREAM_ERROR", "message": "Something wrong happened, please contact support.", "additional_info": "Optional human-readable details" } } ``` - `code`: A stable error code from `StreamErrorCodes`. - `message`: A user‑facing message associated with the code. - `additional_info`: Optional, best‑effort details to aid debugging. ## Common error classes and HTTP status codes The following exception classes map to consistent HTTP status codes: | Exception class | HTTP status | | --- | --- | | `BadRequestError` | 400 | | `UnauthorizedError` | 401 | | `ForbiddenError` | 403 | | `PermissionForbiddenError` | 403 | | `NotFoundError` | 404 | | `GoneError` | 410 | | `UnprocessableEntityError` | 422 | | `DuplicateValueError` | 422 | ## Stream error codes The following codes are used across all Stream API endpoints: | Code | Message | | --- | --- | | `STREAM_ERROR` | Something wrong happened, please contact support. | | `STREAM_UNKNOWN_ERROR` | Unknown exception happened; this exception will be handled as soon as possible. | | `PHONE_ALREADY_REGISTERED` | Phone is already registered. | | `INVALID_PARAMETERS` | Request input is not valid. | | `INVOICE_FINALISED` | Invoice is finalised and cannot undergo this action. | | `PAYMENT_REFUNDED_ALREADY` | Duplicate action; payment has already been refunded. | | `PAYMENT_REFUNDED_FAILED` | Could not refund payment; processor failed. | | `DUPLICATE_CONSUMER` | Consumer already exists. | | `DUPLICATE_PAYMENT` | A payment with the given information already exists. | | `PERMISSION_FORBIDDEN` | Permission forbidden. | | `PRODUCT_USED_IN_FINALIZED_INVOICE` | Product cannot be updated; it is used by a finalized invoice. | | `COUPON_USED_IN_FINALIZED_INVOICE` | Coupon cannot be updated; it is used by a finalized invoice. | --- ## Handling Your Own Notifications This guide explains how to handle your own organization consumer notifications by disabling default notifications and using webhooks for event-based notifications or list APIs for scheduled notifications. ## Overview By default, Stream sends notifications to your organization consumers (customers) for various events like payment confirmations, invoice updates, and subscription changes. If you want to handle these notifications yourself, you can: 1. Disable default org_consumer notifications - Contact support to enable the blocking setting 2. Subscribe to webhook events - For immediate, event-based notifications 3. Use list APIs - For scheduled notifications (reminders, overdue notices, etc.) --- ## Step 1: Disable Default Org Consumer Notifications **Important:** Before disabling default notifications, you must contact our support team to disable default notifications for your organization. This prevents all default notifications from being sent to your organization consumers. Once enabled, you can manage notifications through webhooks and list APIs as described below. --- ## Step 2: Subscribe to Webhook Events For event-based notifications (immediate actions like payment succeeded, invoice accepted, etc.), subscribe to webhook events. To create or manage your webhooks, go to [Webhook Settings](https://app.streampay.sa/settings/webhooks) and subscribe to the relevant event types. For a complete list of available webhook events, payload structure, verification, and delivery details, see the [Webhooks Guide](/webhooks). --- ## Step 3: Use List APIs for Scheduled Notifications For scheduled notifications (time-based reminders like payment due, overdue, etc.), use list APIs with appropriate filters to query for items that need notifications. For example, use the [List Payments API](/api/v2-payments-list), [List Invoices API](/api/v2-invoices-list), or [List Subscriptions API](/api/v2-subscriptions-list). --- ## Stream MCP Server MCP (Model Context Protocol) is a standard way for AI clients to call tools and read resources from external systems. The Stream MCP Server exposes Stream operations (payments, invoices, customers, products, etc.) as MCP tools so clients like Claude Desktop, Cursor, and VS Code can use them directly. **Resources vs tools:** MCP **resources** (such as the OpenAPI documentation) do not require a Stream API key. **Tools** that call the Stream API—payments, customers, invoices, products, coupons, and the rest, **do** require a valid key in your client configuration. ## Main Feature: API Documentation Integration The primary feature of this MCP server is direct integration with Stream API documentation. - It exposes the full OpenAPI spec as an MCP resource. - AI clients can read the latest API schema and stay aligned with current endpoints. - This improves tool accuracy for request fields, validation, and endpoint usage. ## Hosted endpoint Stream hosts the MCP server at: `https://mcp.streampay.sa/mcp` Configure your client to use this URL. Include `Authorization` when you use tools that hit the Stream API; you can omit it if you only read documentation resources. Use the **Connect MCP** button at the top of this page to install the server in Cursor, VS Code, or LM Studio, or to copy the command, URL, or JSON config for other clients. Paste your Stream API key into the optional field in its menu to include the `Authorization` header in the generated config; the key stays in your browser and is never sent anywhere. Without a key, only documentation resources work. --- ## Client configuration (Claude Desktop / Cursor / VS Code) Add this to your MCP config file (`claude_desktop_config.json` or `mcp.json`). Use the `headers` block whenever you rely on tools that require the Stream API (omit it for resources-only use, such as the OpenAPI spec). ```jsonc { "mcpServers": { "stream": { "url": "https://mcp.streampay.sa/mcp", "headers": { // Required for operational tools (payments, customers, etc.); omit if you only read docs resources (e.g. OpenAPI). "Authorization": "Bearer stream_api_key_here" } } } } ``` --- ## Available Tools ### Payment Links | Tool | Description | |---|---| | `create_payment_link` | Create a new checkout / payment link | | `list_payment_links` | Paginated list with optional status filter | | `get_payment_link` | Get a single payment link by ID | | `deactivate_payment_link` | Deactivate / archive a payment link | ### Customers | Tool | Description | |---|---| | `create_customer` | Create a customer with name, email, phone, metadata | | `list_customers` | Paginated list of customers | | `get_customer` | Get a single customer by ID | | `update_customer` | Update customer fields | | `delete_customer` | Soft-delete a customer | ### Products | Tool | Description | |---|---| | `create_product` | Create a one-time or recurring product | | `list_products` | List products with optional type filter | | `get_product` | Get a single product by ID | | `update_product` | Update product name, description, or price | | `archive_product` | Archive a product | ### Coupons | Tool | Description | |---|---| | `create_coupon` | Create a fixed or percentage discount coupon | | `list_coupons` | List coupons with optional status filter | | `get_coupon` | Get a single coupon by ID | | `deactivate_coupon` | Deactivate a coupon | ### Invoices | Tool | Description | |---|---| | `create_invoice` | Create a ZATCA-compliant invoice | | `list_invoices` | List invoices with filters | | `get_invoice` | Get a single invoice by ID | | `send_invoice` | (Re)send an invoice via email / SMS | | `void_invoice` | Void / cancel an unpaid invoice | ### Payments | Tool | Description | |---|---| | `list_payments` | List payments with filters | | `get_payment` | Get payment details | | `refund_payment` | Issue a full or partial refund | ### Resources Reading these resources does not require your Stream API key in the client (unlike the operational tools above). | Resource URI | Description | |---|---| | `stream://docs/openapi` | Full Stream OpenAPI spec (cached, auto-refreshed) — main documentation integration feature | --- ## Add Product Price Add a new price to a product in a different currency. Each product can have one active price per currency. The currency must be enabled in the organization's currency configuration. Request --- ## Auto Charge On Demand Charge a payment using the consumer's latest tokenized card. This endpoint allows organizations to charge a payment using the consumer's latest active tokenized card. The following conditions must be met: - The consumer must have at least one active tokenized card. - The payment must be in an unpaid status (PENDING, PROCESSING, FAILED, etc.). **This is a restricted API and is only available to some organizations.** Request --- ## Cancel an invoice Cancels an invoice and marks it as `CANCELED`. Use this when the invoice should no longer be collected or shown as payable. Request --- ## Cancel Scheduled Subscription Plan Change Cancel a previously-scheduled deferred plan change. No-op (still 200) when no SCHEDULED pending change exists. If the original schedule call had already re-issued the next-cycle invoice (Arm B), the new invoice remains unchanged — it represents the upcoming cycle the consumer was notified about. Request --- ## Cancel Subscription Schedule or complete cancellation and optionally cancel related invoices. Behavior depends on subscription **status**: - **ACTIVE**: Cancellation is **scheduled for the end of the current billing period** (the subscription stays active until then; it is not moved to ``CANCELED`` immediately). - **INACTIVE**, **TRIALING**, **TRIAL_PENDING**: The subscription is **canceled immediately** (status becomes ``CANCELED``). Use ``cancel_related_invoices`` to also cancel ongoing invoices tied to this subscription. Request --- ## Create Branch Create a new branch for the organization. If a branch with the same name already exists, returns the existing branch. Request --- ## Create Consumer Create a new consumer with contact information and payment details. Request --- ## Create Coupon Create a new coupon with discount details and active/inactive status. Request --- ## Create Invite For A User To Join The Organization By Email Send an email invitation for a user to join the organization. An invitation email will be sent to the specified address with a link to accept the invitation. Organization admins can invite users to any branch, while branch admins can only invite to their own branches. Request --- ## Create Invoice Create a new invoice with items and payment details. Important: For most use cases, use the Checkout/Payment Link API (`/v2/payment_links`) instead. It generates a payment link that you can embed in your own web page or redirect to. Use Cases: - Create an invoice and manually mark it as paid for reference - Automated invoice generation Request --- ## Create Metered Subscription Create a new metered subscription linking a METERED product to an organization consumer. The subscription is active immediately when its start time is reached. Usage can begin as soon as the subscription exists, and billing will use the subscription's billable window. Request --- ## Create Payment Link Create a new checkout/payment link for collecting payments. - **One-time products**: Creates an invoice (with its associated payment) when paid. - **Recurring products**: Creates a subscription (with its associated invoice and payment) when paid. **Notes:** - Cannot mix one-time and recurring products in the same payment link. - Installments are automatically disabled for recurring products (subscriptions). - Invoices, subscriptions and payments are created only after a successful payment. Request --- ## Create a consumer portal session Creates a short-lived, single-use portal session token for a merchant-owned consumer. The returned URL can be used to redirect the consumer or embed the portal in an iframe. Request --- ## Create Product Create a new product with pricing and description. Request --- ## Create Subscription Create a new subscription with items, coupons, and payment methods. Important: For most use cases, use the Checkout/Payment Link API (`/v2/payment_links`) instead. It generates a payment link that you can embed in your own web page or redirect to. Use Cases: - Create a subscription and manually marking it as paid for reference - Automated subscription creation Request --- ## Deactivate Metered Subscription Deactivate a metered subscription by setting its end time to now. This subscription stops being billable after this exact time. Note: deactivated subscriptions can still have usage recorded; it won't be billed. Request --- ## Delete Branch Delete a branch from the organization. Request --- ## Delete Consumer Delete a consumer from the organization. Request --- ## Delete Coupon Delete a coupon from the organization. Request --- ## Delete Product Delete a product from the organization. Request --- ## Delete Product Price Archive (soft delete) a product price. Cannot delete the last active price for a product. Each product must have at least one active price. Request --- ## Delete Subscription Freeze Remove a freeze period from a subscription. Deletes the specified freeze period, allowing the subscription to resume normal invoice generation immediately (if the freeze period was active). Request --- ## Freeze Subscription Freeze a subscription to pause invoice generation for a specific period. During the freeze period, no invoices will be generated for the subscription. The subscription will automatically resume after the freeze period ends. Request --- ## Get All Branches List all branches for the organization. Returns branches based on user role: - Organization admins see all branches in the organization - Branch admins and branch users see only branches they're assigned to --- ## Get All Consumers List consumers with pagination, sorting, and optional filters. **Query filters** (all optional; combine as needed): - `search_term` — partial match on name, email, phone, alias, external_id, associated entity name, or associated entity field values **External metadata filters** (optional, combine with other filters): - Use any external metadata key you stored on create/update as a query parameter: `external_metadata.=` - Exact match, case-sensitive. Multiple keys are ANDed. - Example: if a consumer was created with `"external_metadata": {"segment": "enterprise", "tier": "gold"}`, filter with `?external_metadata.segment=enterprise&external_metadata.tier=gold` **Pagination**: `page` (default 1), `limit` (default 10, max 100) **Sorting**: `sort_field` (`name`, `created_at`, `email`, `phone_number`, `external_id`; default `created_at`), `sort_direction` (`asc` or `desc`; default `desc`) **Example**: `/api/v2/consumers?search_term=Acme&external_metadata.segment=enterprise&page=1&limit=20` Request --- ## Get Branch By Id Retrieve detailed information about a specific branch by its ID. Request --- ## Get Consumer Retrieve detailed information about a specific consumer by their ID. Request --- ## Get Coupon Retrieve detailed information about a specific coupon including redemption history. Request --- ## Get Invoice Retrieve detailed information about a specific invoice by its ID. Request --- ## Get Metered Subscription Retrieve a metered subscription. Request --- ## Get Payment Retrieve detailed information about a specific payment by its ID. Request --- ## Get Payment Link Retrieve detailed information about a specific payment link by its ID. Request --- ## Get Product Retrieve detailed information about a specific product by its ID. Request --- ## Get Subscription Retrieve detailed information about a specific subscription by its ID. Request --- ## Get User And Organization Info Retrieve information about the authenticated user and their organization. It requires a valid authentication token and can be used to verify API connectivity and retrieve user context. This endpoint returns details about the currently authenticated user and their associated organization. --- ## List Coupons List all coupons with pagination, filtering, and sorting options. Request --- ## List Invoices List all invoices with pagination, filtering, and sorting options. Request --- ## List Metered Subscriptions List metered subscriptions. Request --- ## List Payment Links List all payment links with pagination, filtering, and sorting options. Request --- ## List Payments List all payments with pagination, filtering, and sorting options. Payments can be filtered by invoice IDs, statuses, date ranges, or search terms. Request --- ## List Products List products with pagination, sorting, and optional filters. **Query filters** (all optional; combine as needed): - `search_term` — partial match on product name or description - `active` — `true` for active products only, `false` for inactive only - `type` — product type: `RECURRING`, `ONE_OFF`, or `METERED` (when enabled) - `currency` — only products with an active price in this ISO currency code (e.g. `SAR`) **External metadata filters** (optional, combine with other filters): - Use any external metadata key you stored on create/update as a query parameter: `external_metadata.=` - Exact match, case-sensitive. Multiple keys are ANDed. - Example: if a consumer was created with `"external_metadata": {"segment": "enterprise", "tier": "gold"}`, filter with `?external_metadata.segment=enterprise&external_metadata.tier=gold` **Pagination**: `page` (default 1), `limit` (default 10, max 100) **Sorting**: `sort_field` (`price`, `name`, `created_at`; default `created_at`), `sort_direction` (`asc` or `desc`; default `desc`) **Example**: `/api/v2/products?active=true&type=RECURRING&external_metadata.category=saas¤cy=SAR` Request --- ## List Subscription Freezes List all freeze periods for a specific subscription. Request --- ## List Subscriptions List all subscriptions with pagination, filtering, and sorting options. Request --- ## Mark Payment As Paid Manually mark a payment as paid (full or partial). Record a payment received through manual methods (CASH, BANK_TRANSFER, CARD, or QURRAH). **Full:** omit ``amount`` or pass ``amount == payment.amount``. **Partial:** pass ``amount < payment.amount`` with remainder greater than 1 in the payment currency (not allowed while UNDER_REVIEW). - Selected payment amount is reduced and marked SUCCEEDED. - A new PENDING ``SPLIT_REMAINDER`` payment is created for the remainder. - ``invoice.type`` is unchanged (ONE_OFF stays ONE_OFF). - Invoice completes only when every payment is paid. Request --- ## Record Metered Usage Record metered usage events in bulk Request --- ## Refund Payment Process a refund for a payment. Refunds settle the requested amount or the remaining refundable balance for the selected payment. If that payment is part of a shared Moyasar charge, set ``payment_is_part_of_related_set`` to true to distribute the refund across related payments in invoice order. Request --- ## Uncancel Subscription Withdraw a pending cancellation so the subscription continues after the current billing period. Only **ACTIVE** subscriptions are accepted. If the subscription is not scheduled to be cancelled, the request succeeds as a no-op. Request --- ## Update Branch Update branch information. If a branch with the same name already exists, returns an error. Request --- ## Update Consumer Update consumer information such as contact details or payment methods. Request --- ## Update Coupon Update coupon details such as discount amount or active status. Request --- ## Update Invoice In Place Partially update specific invoice fields in-place. This endpoint allows updating only the fields specified in the request. Use for quick changes such as updating the description or scheduled date. Request --- ## Update Payment Link Coupons Update link-level and item-level coupons on a payment link. Recomputes totals. New checkouts use the updated coupons; payments already in progress are unchanged. Request --- ## Update Payment Link Status Update the status of a payment link. Allows activating or deactivating a payment link. When deactivated, the link will no longer accept new payments. Existing payments in progress are not affected. Request --- ## Update Product Update product details such as name, price, or description. Request --- ## Update Product Price Update a product price. This follows an immutable pricing pattern: the old price is archived and a new price is created with the updated values. Request --- ## Update Subscription Update subscription items, coupons, billing interval, description, auto-cancel cycles, or payment methods. Updatable fields: - **Items** (products and quantities) — deferred to the next cycle. - **Subscription-level coupons** — same deferral as items. - **Recurring interval / interval count** (e.g. monthly ↔ yearly) — deferred to the next cycle. - **Description** — applied immediately. - **Until cycle number** (auto-cancel) — applied immediately. - **Payment method overrides** — applied immediately. Any change to items, coupons, or interval is recorded as a *pending change* that takes effect at ``current_period_end``. The response carries a ``pending_change`` block describing the deferred change, including a ``next_invoice_preview`` and the per-line diff. If the next-cycle invoice has already been generated and is unpaid, it is automatically cancelled and re-issued with the new amount. To change anything else (e.g. the consumer / payer), cancel the subscription and create a new one. Use ``DELETE /pending-change`` to revert a previously scheduled change before it takes effect. Request --- ## Update Subscription Freeze Update an existing subscription freeze period. Allows modifying the start and end dates of a freeze period. The subscription will remain frozen during the updated period. Request --- ## Express SDK For the complete Express SDK documentation, please visit our GitHub repository: **[📦 @streamsdk/express on GitHub →](https://github.com/streampayments/streamsdk-express)** ## Quick Links - **[Installation & Quick Start](https://github.com/streampayments/streamsdk-express#installation)** - **[API Documentation](https://github.com/streampayments/streamsdk-express#api-documentation)** - **[Code Examples](https://github.com/streampayments/streamsdk-express/tree/main/examples)** - **[Demo Application](https://github.com/streampayments/stream-express-sdk-demo)** - **[npm Package](https://www.npmjs.com/package/@streamsdk/express)** ## Overview The Stream Express SDK provides a clean, declarative way to integrate Stream payments into your Express.js applications. Built on top of [@streamsdk/typescript](https://www.npmjs.com/package/@streamsdk/typescript), it offers drop-in handlers for checkout flows and webhook processing. **Key Benefits:** - 🚀 **Simple Integration** - Add payment processing in minutes - 🔒 **Type-Safe** - Full TypeScript support with type definitions - 🎯 **Event-Driven** - Clean event handlers for webhook processing - 🔄 **Flexible** - Supports guest and authenticated checkout flows - 📦 **Lightweight** - Minimal dependencies (~7KB) - 🔗 **Auto-Updates** - Inherits stream-sdk updates automatically ## Installation ```bash npm install @streamsdk/express ``` ## Quick Example ```typescript const app = express(); app.use(express.json()); // Payment link handler app.get("/checkout", Checkout({ apiKey: process.env.STREAM_API_KEY!, successUrl: "https://myapp.com/success", returnUrl: "https://myapp.com/cancel", })); // Webhook handler with events app.post("/webhooks/stream", Webhooks({ apiKey: process.env.STREAM_API_KEY!, onPaymentSucceeded: async (data) => { console.log("Payment succeeded:", data); // Update your database, send confirmation emails, etc. }, onPaymentFailed: async (data) => { console.log("Payment failed:", data); }, onInvoiceCreated: async (data) => { console.log("Invoice created:", data); }, })); app.listen(3000); ``` For complete documentation, examples, and API reference, visit the [GitHub repository](https://github.com/streampayments/streamsdk-express). ## Demo Application Want to see it in action? Clone and run the **[Express SDK demo repository](https://github.com/streampayments/stream-express-sdk-demo)** locally for a quick working example. ## Related SDKs - **[TypeScript SDK](/sdks/typescript)** - Core SDK for Node.js/TypeScript - **[API Reference](/api/v2-payment-links-create)** - REST API documentation - **[Webhooks Guide](/webhooks)** - Webhook event reference --- ## TypeScript SDK For the complete TypeScript SDK documentation, please visit our GitHub repository: **[📦 @streamsdk/typescript on GitHub →](https://github.com/streampayments/streamsdk-typescript)** ## Quick Links - **[Installation & Quick Start](https://github.com/streampayments/streamsdk-typescript#installation)** - **[API Documentation](https://github.com/streampayments/streamsdk-typescript#api-documentation)** - **[Code Examples](https://github.com/streampayments/streamsdk-typescript/tree/main/examples)** - **[npm Package](https://www.npmjs.com/package/@streamsdk/typescript)** ## Overview The Stream TypeScript SDK provides a complete Node.js/TypeScript interface for: - 🔐 Secure API Key authentication - 👥 Customer (Consumer) management - 📦 Product catalog management - 💳 Payment link creation - 🔄 Subscription handling - 🧾 Invoice generation - 🎟️ Coupon & discount management ## Installation ```bash npm install @streamsdk/typescript ``` ## Quick Example ```typescript const client = StreamSDK.init(process.env.STREAM_API_KEY!); const result = await client.createSimplePaymentLink({ name: "Monthly Subscription", amount: 99.99, consumer: { email: "customer@example.com", name: "Ahmad Ali", phone: "+966501234567", }, product: { name: "Premium Plan", price: 99.99, }, successRedirectUrl: "https://yourapp.com/success", failureRedirectUrl: "https://yourapp.com/failure", }); console.log("Payment URL:", result.paymentUrl); ``` For complete documentation, examples, and API reference, visit the [GitHub repository](https://github.com/streampayments/streamsdk-typescript). ## Related SDKs - **[Express.js SDK](/sdks/express)** - Framework adapter for Express.js applications - **[API Reference](/api/v2-payment-links-create)** - REST API documentation