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

# Commercial performance over a period

> Business view of the channel over an arbitrary period, complementing the
operational `/dashboard`: volume, your own revenue, effective take rate,
average basket and cancellations measured in value.

Metrics are computed on bookings **created** within the period whose status
became firm, that is `CONFIRMED`, `COMPLETED`, `CANCELLED`, `REFUND_PENDING`
or `REFUNDED`. Bookings still `PENDING` or `PAYMENT_PROCESSING` are excluded,
since they may never be paid.

`gross_merchandise_value` is the total charged to customers and therefore
includes your surcharge. `partner_revenue` is what you keep, commission share
plus surcharge. Cancellation figures cover the cancelled subset of the same
bookings, valued at their original customer price, so
`cancellation_rate_percent` is a share of value and not a share of headcount.

The period is capped at 366 days. Read-only.




## OpenAPI

````yaml /api-reference/openapi.yaml get /metrics/commercial
openapi: 3.1.0
info:
  title: SafarAPI
  version: 1.0.0
  summary: B2B API for banking partners — Safariat catalog and booking
  description: >
    SafarAPI lets banking partners integrate the Safariat travel catalog into
    their

    authenticated customer areas and book on behalf of their own end customers
    who are

    already authenticated and KYC-verified on the bank side.


    ## Channel model

    - The traveler never authenticates with Safariat.

    - Payment is collected on the banking platform (out-of-band from Safariat).

    - The bank submits the HMAC-signed payment confirmation at booking time.

    - Monthly post-payment settlement (see `GET /settlements`).


    ## Authentication

    Bearer API key in the `Authorization` header. Format
    `sk_live_<prefix>_<secret>` (production)

    or `sk_test_<prefix>_<secret>` (sandbox). The secret is shown only once at
    key generation —

    Safariat stores only an argon2id hash.


    ## Write request signing

    Production write requests (`POST`, `PUT`, `DELETE`) additionally require:

    - `X-Timestamp`: Unix timestamp in seconds, validity window ±5 minutes.

    - `X-Signature`: `hex(HMAC_SHA256(secret,
    "{timestamp}\n{method}\n{path}\n{body}"))`.

    - `Idempotency-Key`: opaque string unique per operation (UUID v4
    recommended),
      retained for 24 h. Replayed responses carry an `Idempotent-Replayed: true` header.

    Request signing applies only to production `sk_live_*` keys. Sandbox
    `sk_test_*` keys are

    exempt from request signing: `X-Signature` and `X-Timestamp` are not
    required for writes

    in the sandbox (so the developer-portal "Try it" playground works end to
    end).

    `Idempotency-Key` is still required on writes in both environments.


    ## Error format

    All errors follow the `Error` schema with a stable i18n code, an English
    message, and the

    `request_id` for correlation with Safariat logs.


    ## Versioning

    URL-based versioning (`/v1`). No breaking changes within a version. Removals
    are announced

    6 months in advance via the `Sunset` header (RFC 8594).


    ## Rate limiting

    Default limit of 600 req/min per key, contractually adjustable. The
    `X-RateLimit-Limit`,

    `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers are returned on
    every response.

    Exceeding the limit → `429 Too Many Requests` plus the `Retry-After` header.


    ## Sandbox

    An `sk_test_*` key targets the same infrastructure as production but
    isolates bookings in

    `test_mode=true`: no settlement is issued, webhooks carry `test: true`, and
    operators are

    never notified. Traveller emails are still delivered, to the address
    supplied in the request

    and with a `[TEST]` subject prefix, so that the traveller journey can be
    validated end to end

    before going live.
  contact:
    name: SafarAPI Team
    email: api@safariat.ma
    url: https://developers.safariat.ma
  license:
    name: Proprietary — use subject to a signed partner agreement
  termsOfService: https://safariat.ma/legal/partner-terms
servers:
  - url: https://api.safarapi.com/api/partner/v1
    description: >-
      Production and sandbox share this host. The environment is selected by the
      API key prefix: sk_live_ targets production, sk_test_ targets the sandbox.
security:
  - apiKey: []
tags:
  - name: Meta
    description: Metadata for the current key, service health.
  - name: Catalog
    description: Browsing the Safariat adventure catalog.
  - name: Quotes
    description: Price-locked quotes (30 min TTL) ahead of booking.
  - name: Bookings
    description: Creating, retrieving, and cancelling bookings.
  - name: Settlements
    description: Monthly Safariat → partner payout invoices.
  - name: Webhooks
    description: Managing endpoints that receive Safariat events.
  - name: Team
    description: Members of the partner console workspace and their roles.
  - name: Reviews
    description: Read-only access to traveler reviews on the Safariat catalog.
  - name: Files
    description: Presigned uploads for partner-supplied files (currently review photos).
paths:
  /metrics/commercial:
    get:
      tags:
        - Dashboard
      summary: Commercial performance over a period
      description: >
        Business view of the channel over an arbitrary period, complementing the

        operational `/dashboard`: volume, your own revenue, effective take rate,

        average basket and cancellations measured in value.


        Metrics are computed on bookings **created** within the period whose
        status

        became firm, that is `CONFIRMED`, `COMPLETED`, `CANCELLED`,
        `REFUND_PENDING`

        or `REFUNDED`. Bookings still `PENDING` or `PAYMENT_PROCESSING` are
        excluded,

        since they may never be paid.


        `gross_merchandise_value` is the total charged to customers and
        therefore

        includes your surcharge. `partner_revenue` is what you keep, commission
        share

        plus surcharge. Cancellation figures cover the cancelled subset of the
        same

        bookings, valued at their original customer price, so

        `cancellation_rate_percent` is a share of value and not a share of
        headcount.


        The period is capped at 366 days. Read-only.
      operationId: getCommercialMetrics
      parameters:
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date
          description: First day of the period, inclusive.
          example: '2026-08-01'
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date
          description: Last day of the period, inclusive.
          example: '2026-08-31'
      responses:
        '200':
          description: Commercial snapshot for the requested period.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommercialMetrics'
              example:
                period:
                  from: '2026-08-01'
                  to: '2026-08-31'
                bookings_count: 42
                travelers_count: 96
                gross_merchandise_value:
                  amount: '261000.00'
                  currency: MAD
                partner_revenue:
                  amount: '4350.00'
                  currency: MAD
                amount_due_to_safariat:
                  amount: '256650.00'
                  currency: MAD
                effective_take_rate_percent: '1.67'
                average_basket:
                  amount: '6214.29'
                  currency: MAD
                travelers_per_booking: '2.29'
                cancelled_bookings_count: 3
                cancelled_value:
                  amount: '18500.00'
                  currency: MAD
                cancellation_rate_percent: '7.09'
                cancellations_by_reason:
                  - reason: customer_request
                    bookings_count: 2
                    value:
                      amount: '12500.00'
                      currency: MAD
                  - reason: supplier_cancellation
                    bookings_count: 1
                    value:
                      amount: '6000.00'
                      currency: MAD
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
components:
  schemas:
    CommercialMetrics:
      type: object
      required:
        - period
        - bookings_count
        - travelers_count
        - gross_merchandise_value
        - partner_revenue
        - amount_due_to_safariat
        - effective_take_rate_percent
        - average_basket
        - travelers_per_booking
        - cancelled_bookings_count
        - cancelled_value
        - cancellation_rate_percent
        - cancellations_by_reason
      properties:
        period:
          type: object
          required:
            - from
            - to
          properties:
            from:
              type: string
              format: date
            to:
              type: string
              format: date
        bookings_count:
          type: integer
          format: int64
          description: Firm bookings created within the period.
        travelers_count:
          type: integer
          format: int64
          description: Adults plus children across those bookings.
        gross_merchandise_value:
          $ref: '#/components/schemas/Money'
          description: Total charged to customers, your surcharge included.
        partner_revenue:
          $ref: '#/components/schemas/Money'
          description: What you keep = commission share + surcharge.
        amount_due_to_safariat:
          $ref: '#/components/schemas/Money'
          description: Total reversed to Safariat over the period.
        effective_take_rate_percent:
          type: string
          description: >-
            `partner_revenue / gross_merchandise_value`, as a percentage with
            two decimals. `0.00` on an empty period.
          example: '1.67'
        average_basket:
          $ref: '#/components/schemas/Money'
          description: Gross merchandise value divided by the number of bookings.
        travelers_per_booking:
          type: string
          description: Two decimals. `0.00` on an empty period.
          example: '2.29'
        cancelled_bookings_count:
          type: integer
          format: int64
        cancelled_value:
          $ref: '#/components/schemas/Money'
          description: Original customer price of the cancelled bookings.
        cancellation_rate_percent:
          type: string
          description: Share of value, not of headcount.
          example: '7.09'
        cancellations_by_reason:
          type: array
          description: >
            Ordered by decreasing booking count. `unspecified` groups
            cancellations recorded

            without a reason. Only the reason code is reported: any free-text
            note you attach

            when cancelling is stripped, so the breakdown stays aggregated by
            cause rather than

            fragmenting into one row per note.
          items:
            type: object
            required:
              - reason
              - bookings_count
              - value
            properties:
              reason:
                type: string
                example: customer_request
              bookings_count:
                type: integer
                format: int64
              value:
                $ref: '#/components/schemas/Money'
    Money:
      type: object
      required:
        - amount
        - currency
      properties:
        amount:
          type: string
          pattern: ^-?\d+\.\d{2}$
          description: Amount as a decimal string with 2 decimals to preserve precision.
          example: '3200.00'
        currency:
          type: string
          enum:
            - MAD
          description: Currency — MAD only in V1.
          example: MAD
    Error:
      type: object
      required:
        - code
        - message
        - request_id
      properties:
        code:
          type: string
          description: |
            Stable, documented i18n code. Form `domain.subdomain.detail`
            (e.g. `payment.amount.mismatch`, `auth.api_key.invalid`).
          example: validation.required.field
        message:
          type: string
          description: >-
            English message (for bank logs). API errors are never localized on
            the Safariat side.
          example: Field 'rate_pack_id' is required.
        request_id:
          type: string
          description: Unique request ID on the Safariat side.
        details:
          type: object
          description: Optional structured details (offending field, expected value, etc.).
          additionalProperties: true
  responses:
    BadRequest:
      description: >
        Malformed request (syntactic validation).


        A request body carrying a field this endpoint does not declare is
        rejected with

        `partner.request.field.unknown`, naming the offending field. Unknown
        fields are never

        ignored: a misspelling would otherwise drop data silently, and on `POST
        /quotes` that

        means quoting a party smaller than the one you sent.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missingField:
              value:
                code: validation.required.field
                message: Field 'rate_pack_id' is required.
                request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
            unknownField:
              value:
                code: partner.request.field.unknown
                message: Unknown field 'children_ages' in the request body.
                request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHD
    Unauthorized:
      description: API key missing, invalid, expired, or incorrect HMAC signature.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing:
              value:
                code: auth.api_key.missing
                message: Authorization header is required.
                request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
            invalid:
              value:
                code: auth.api_key.invalid
                message: The provided API key is invalid or has been revoked.
                request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
            signatureInvalid:
              value:
                code: auth.signature.invalid
                message: HMAC signature does not match.
                request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
            timestampSkew:
              value:
                code: auth.timestamp.skew
                message: Timestamp is more than 5 minutes off server time.
                request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
    Forbidden:
      description: Authenticated but not authorized (missing scope, non-allowlisted IP).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    TooManyRequests:
      description: Rate limit exceeded for the current key.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/XRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/XRateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: rate_limit.exceeded
            message: Rate limit exceeded for this API key.
            request_id: 01HXY2ZRPM3R8K9PSBTRQYNFHC
  headers:
    RetryAfter:
      schema:
        type: integer
      description: Seconds to wait before the next attempt (RFC 7231).
    XRateLimitLimit:
      schema:
        type: integer
      description: Request limit per minute for the current key.
    XRateLimitRemaining:
      schema:
        type: integer
      description: Requests remaining in the current window.
    XRateLimitReset:
      schema:
        type: integer
        format: int64
      description: Unix timestamp (s) at which the window resets.
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: sk_live_<prefix>_<secret> or sk_test_<prefix>_<secret>
      x-default: sk_test_DEMO0000_replace_with_your_sandbox_key
      description: >
        API key authentication. Issued by the Safariat admin or via the partner
        portal.

        The secret is shown only once at generation.


        Production `sk_live_*` keys must additionally sign every write request

        (`POST`/`PUT`/`DELETE`) with the `X-Timestamp` and `X-Signature`
        headers.

        Sandbox `sk_test_*` keys are exempt from request signing: `X-Signature`
        and

        `X-Timestamp` are not required for writes in the sandbox (so the
        developer-portal

        "Try it" playground works end to end). The `Idempotency-Key` header
        remains

        required on writes in both environments.

````