> ## Documentation Index
> Fetch the complete documentation index at: https://docs.segmnt.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Payment provider callback (internal)

> Called only by the payment provider. Not for customers. The request is
checked with the provider's own signature or secret token, so it has no
segmnt authentication. The body is the provider's own format.

While paid billing is paused, a valid event is acknowledged with `204`
and no plan changes. The signature is still checked.

The webhook body is never trusted. A verified event is used only to find
the provider invoice; the invoice is then read back from the provider API,
and its amount and currency are checked against the plan's board-fixed
price. Only then is the plan applied: the payment, the VAT invoice, the
subscription and the account plan are written in one batch, so the same
event can be replayed safely (a provider retries). A refunded invoice
reverses an applied payment: the subscription is set to `canceled` and the
account returns to the free plan. A verified event that does not match
what was ordered answers 400 and changes no plan.




## OpenAPI

````yaml /api/openapi.yaml post /v1/billing/providers/{provider}/webhook
openapi: 3.1.0
info:
  title: segmnt API
  version: 0.6.1
  summary: >-
    Video hosting and signed HLS playback for course platforms, agencies and
    training companies.
  description: >
    First draft of the segmnt API contract (task A2). It is a contract for the
    MVP

    and will change while we build it.


    ## Authentication

    - **API key** (`apiKey`): `Authorization: Bearer <key>`. A key belongs to
    one
      project. It can only call routes under `/v1/projects/{projectId}` for that
      project. Send it only from your server, never from a browser.
    - **Dashboard session** (`session`): cookie set after SSO sign-in (Google,
    GitHub,
      Reddit, Apple). It can call every route for projects in the signed-in account.
      Account routes (projects, API keys, billing) accept only the session.
      The cookie is `HttpOnly; Secure; SameSite=Lax`. Requests with a body must be
      `application/json`; other content types get `415`.

    The SSO sign-in routes live under `/auth/*` (task A8): the browser is

    redirected to the provider and back, then the `segmnt_session` cookie

    authenticates the account routes.


    ## Rate limits

    Every `/v1/**` and `/auth/**` request is rate limited by client IP (task

    A17): `/v1/**` allows 1,200 requests per minute and `/auth/**` allows 60 per

    minute. `GET /health` and `OPTIONS` preflight are never counted. A blocked

    request gets `429` with a `Retry-After` header and `code: "rate_limited"`.


    ## Video lifecycle

    `uploading` → `processing` → `ready`, or `failed`. Our team can move a video
    to

    `held` (it does not play) or back to `ready`. Moving to `ready`, `failed`

    or `held` sends a webhook.

    A held or deleted video stops playing within about a minute, also for tokens

    issued before.


    ## Playback

    Videos never have a public URL. Your server asks for a playback token for
    one

    video and one verified domain. The token goes in the `token` query parameter

    of the master playlist URL:

    `https://test-cdn.segmnt.cloud/v/{videoId}/master.m3u8?token={token}`.

    The CDN rewrites every playlist it returns and adds the same `?token=` to

    each link inside, so a player only needs the master playlist URL.


    Free plans must play with `embedUrl` (our player, with our logo).

    `playlistUrl` is returned only on plans with `whiteLabel`.


    **Domain check.** The CDN takes the `Origin` header, or the origin of

    `Referer` when there is no `Origin`. It must be exactly `https://{domain}`,

    where `{domain}` is the token's domain:

    - scheme `https` only; no port;

    - host compared without case (`Courses.Example.com` =
    `courses.example.com`);

    - exact host only: subdomains do not match (`www.courses.example.com` needs
      its own verified domain).

    Requests from our embed page (on the CDN host) pass the domain check; the

    embed page only loads inside a frame on the token's domain

    (`Content-Security-Policy: frame-ancestors`).


    **Expiry.** A token is expired when the CDN clock is at or after

    `expiresAt`. There is no clock skew allowance. Note: browsers keep segments

    in their cache for 1 day and playlists for 30 seconds, so test expiry with a

    fresh client or no browser cache.


    **CDN errors.** Every refused request has the header

    `X-Segmnt-Error: <code>` and an `application/problem+json` body with the
    same

    `code`. A token check runs in this order; the first failure wins:


    | Status | `code` | When |

    |---|---|---|

    | `404` | `not_found` | Path is not a video file we serve. |

    | `403` | `token_missing` | No `token` query parameter. |

    | `403` | `token_invalid` | Bad format or signature, or the token is for
    another video. |

    | `403` | `token_expired` | Token is past `expiresAt`. |

    | `403` | `origin_missing` | No `Origin` and no `Referer` header. |

    | `403` | `domain_mismatch` | `Origin` / `Referer` is not
    `https://{domain}`. |

    | `403` | `video_unavailable` | Video is held or deleted. |

    | `403` | `plan_limit_reached` | The account is over its free limit, or the
    request is for a quality above the plan's maximum. |


    A held or deleted video gets `video_unavailable` within about a minute, also
    for

    tokens issued before. After a delete, files are removed and the CDN can also

    answer `404` `not_found`. A request for a quality above your plan's maximum

    is also `plan_limit_reached`, even when the account is inside its limit; the

    master playlist only lists the qualities your plan allows.


    ## Conventions

    - IDs are strings with a type prefix: `acc_`, `usr_`, `prj_`, `vid_`,
    `upl_`, `key_`, `dom_`, `whe_`, `evt_`.

    - Times are RFC 3339 in UTC.

    - Lists use cursor pagination: pass `nextCursor` back as `cursor`.

    - Errors use `application/problem+json` (RFC 9457) with a stable `code`.

    - `Idempotency-Key` is scoped to the account and the route. The same key
    with
      a different body gets `422`.
    - Money is an integer in the smallest unit of the currency (halalas for
    SAR).
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
  contact:
    name: segmnt engineering
servers:
  - url: https://test-api.segmnt.cloud
    description: TEST environment
security:
  - apiKey: []
  - session: []
tags:
  - name: Auth
    description: |
      SSO sign-in for the dashboard. No passwords are stored. Sign-in is a
      browser redirect flow: `/auth/{provider}/start` → the provider →
      `/auth/{provider}/callback`, which sets the `segmnt_session` cookie.
      Launch providers: Google and GitHub. Apple and Reddit come later.
  - name: Projects
    description: An account has many projects. Agencies use one project per client.
  - name: API keys
    description: Server-side keys. Each key belongs to one project.
  - name: Domains
    description: >-
      Sites allowed to play the project's videos. A domain must be verified with
      a DNS TXT record.
  - name: Uploads
    description: Direct upload from the client to storage with pre-signed multipart URLs.
  - name: Videos
    description: Uploaded videos and their processing status.
  - name: Playback
    description: Signed playback tokens.
  - name: Webhooks
    description: >
      HTTPS endpoints that receive video events. The MVP has no "send test
      event"

      route and no delivery log. To test, register your own receiver and upload

      a short video.
  - name: Usage
    description: Stored and watched time.
  - name: Billing
    description: Plans and checkout through the payment provider's hosted page.
  - name: Admin
    description: |
      Internal routes for our team (A12 abuse/review and A13 bans). Not for
      customers. The page is at `/admin` behind Cloudflare Access. Every admin
      route is gated by `adminAccess` (a Cloudflare Access JWT, or the TEST
      `ADMIN_TOKEN`). Internal helper routes are called by our own Workers with
      `internalSecret`.
    x-internal: true
  - name: System
    description: Health check.
paths:
  /v1/billing/providers/{provider}/webhook:
    parameters:
      - name: provider
        in: path
        required: true
        schema:
          type: string
          enum:
            - moyasar
            - polar
    post:
      tags:
        - Billing
      summary: Payment provider callback (internal)
      description: >
        Called only by the payment provider. Not for customers. The request is

        checked with the provider's own signature or secret token, so it has no

        segmnt authentication. The body is the provider's own format.


        While paid billing is paused, a valid event is acknowledged with `204`

        and no plan changes. The signature is still checked.


        The webhook body is never trusted. A verified event is used only to find

        the provider invoice; the invoice is then read back from the provider
        API,

        and its amount and currency are checked against the plan's board-fixed

        price. Only then is the plan applied: the payment, the VAT invoice, the

        subscription and the account plan are written in one batch, so the same

        event can be replayed safely (a provider retries). A refunded invoice

        reverses an applied payment: the subscription is set to `canceled` and
        the

        account returns to the free plan. A verified event that does not match

        what was ordered answers 400 and changes no plan.
      operationId: receivePaymentWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '204':
          description: >-
            Event accepted (also when it was already processed, or was not a
            paid payment).
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '502':
          description: The provider API could not be read. The provider will retry.
      security: []
components:
  responses:
    BadRequest:
      description: The request is not valid.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Unauthorized:
      description: Missing or wrong API key or session.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    Problem:
      type: object
      required:
        - type
        - title
        - status
        - code
      properties:
        type:
          type: string
          format: uri-reference
        title:
          type: string
        status:
          type: integer
        code:
          type: string
          description: Stable error code for programs.
          examples:
            - plan_limit_reached
        detail:
          type: string
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: Project API key. Server-side only.
    session:
      type: apiKey
      in: cookie
      name: segmnt_session
      description: Dashboard session cookie, set after SSO sign-in.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.