openapi: 3.1.0
info:
  title: DPD API
  version: '2'
  summary: Read your DPD account's storefronts, products, purchases, customers and subscribers.
  description: |
    The DPD API is a read-only window into your DPD account. With the API you
    are able to view all of the data that is available in your account.

    Authenticate with HTTP Basic authentication: your DPD username, and the
    API password from your profile in the DPD admin.

    ## Pagination

    Collections return at most 100 records per page when you pass `page`.
    Without `page`, the API tries to return every record, which isn't
    guaranteed to work on large accounts. Start with `page=1` and count up
    until you get a `404` with `{"status": "NOTFOUND"}`.

    An empty result is also a `404` with `{"status": "NOTFOUND"}`, not an
    empty list.

    ## Dates and times

    Responses give dates as UNIX timestamps (seconds). Date filters accept
    anything PHP's `strtotime()` accepts, in your account's time zone unless
    the value includes one, such as ISO 8601 (`2004-02-12T15:19:21+00:00`) or
    RFC 2822 (`Thu, 21 Dec 2000 16:01:07 +0200`).

    ## Money

    Amounts are strings with two decimal places, such as `"19.99"`.
  contact:
    name: DPD support
    url: https://getdpd.com
servers:
  - url: https://api.getdpd.com/v2
security:
  - basicAuth: []
tags:
  - name: General
    description: Check the API is up, and verify purchase notifications.
  - name: Storefronts
    description: Storefronts are also called stores and websites in the DPD admin.
  - name: Products
    description: The products you sell, with their price points.
  - name: Purchases
    description: Orders, with their line items, coupons, customer and payment transactions.
  - name: Customers
    description: Everyone who has bought from you.
  - name: Subscribers
    description: Members of a subscription storefront, and their subscription status.

paths:
  /:
    get:
      tags: [General]
      operationId: ping
      summary: Ping
      description: Checks that the API is up and your credentials work.
      responses:
        '200':
          description: The API is up and your credentials are valid.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    const: SUCCESS
        '401':
          $ref: '#/components/responses/Unauthorized'

  /notification/verify:
    post:
      tags: [General]
      operationId: verifyNotification
      summary: Verify a purchase notification
      description: |
        Checks that a purchase notification POSTed to your site by DPD's URL
        integration really came from DPD. POST back every parameter you
        received, unchanged, including `verify_sign`.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [verify_sign]
              properties:
                verify_sign:
                  type: string
                  description: The signature DPD sent with the notification.
              additionalProperties:
                type: string
      responses:
        '200':
          description: Whether the notification is genuine.
          content:
            text/plain:
              schema:
                type: string
                enum: [VERIFIED, INVALID]
        '401':
          $ref: '#/components/responses/Unauthorized'

  /storefronts:
    get:
      tags: [Storefronts]
      operationId: listStorefronts
      summary: List storefronts
      parameters:
        - $ref: '#/components/parameters/page'
      responses:
        '200':
          description: The storefronts in your account.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Storefront'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /storefronts/{id}:
    get:
      tags: [Storefronts]
      operationId: getStorefront
      summary: Get a storefront
      parameters:
        - $ref: '#/components/parameters/id'
      responses:
        '200':
          description: The storefront.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Storefront'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /products:
    get:
      tags: [Products]
      operationId: listProducts
      summary: List products
      parameters:
        - name: storefront_id
          in: query
          description: Only products in this storefront. Defaults to all storefronts.
          schema:
            type: integer
        - $ref: '#/components/parameters/page'
      responses:
        '200':
          description: The products in your account.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Product'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /products/{id}:
    get:
      tags: [Products]
      operationId: getProduct
      summary: Get a product
      description: |
        A product's image comes in several sizes: `50x50`, `100x100`,
        `125x125`, `150x150`, `250x250`, `300x300`, `350x350`, `400x400`,
        `450x450`, `500x500`, `550x550`, `600x600`, `800x800` and `1000x1000`.
        The URL for a size is
        `https://d2beuh40lcdzfb.cloudfront.net/products/{id}/{size}/{image_file_name}`.
      parameters:
        - $ref: '#/components/parameters/id'
      responses:
        '200':
          description: The product.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /purchases:
    get:
      tags: [Purchases]
      operationId: listPurchases
      summary: List purchases
      parameters:
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/PurchaseStatus'
        - name: product_id
          in: query
          schema:
            type: integer
        - name: storefront_id
          in: query
          schema:
            type: integer
        - name: customer_id
          in: query
          schema:
            type: integer
        - name: customer_email
          in: query
          description: Customer email address. Matches the start of the address, not case sensitive.
          schema:
            type: string
        - name: customer_first_name
          in: query
          description: Matches the start of the name, not case sensitive.
          schema:
            type: string
        - name: customer_last_name
          in: query
          description: Matches the start of the name, not case sensitive.
          schema:
            type: string
        - $ref: '#/components/parameters/date_min'
        - $ref: '#/components/parameters/date_max'
        - name: total
          in: query
          description: Purchase total, compared with `total_op`.
          schema:
            type: string
            examples: ['19.99']
        - name: total_op
          in: query
          description: How to compare `total`.
          schema:
            type: string
            enum: [eq, ne, gt, lt]
            default: eq
        - name: ship
          in: query
          description: Only purchases with physical goods that haven't shipped yet.
          schema:
            type: boolean
        - $ref: '#/components/parameters/page'
      responses:
        '200':
          description: The matching purchases.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Purchase'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /purchases/{id}:
    get:
      tags: [Purchases]
      operationId: getPurchase
      summary: Get a purchase
      parameters:
        - $ref: '#/components/parameters/id'
      responses:
        '200':
          description: The purchase.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Purchase'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /purchases/{id}/reactivate:
    post:
      tags: [Purchases]
      operationId: reactivatePurchase
      summary: Reactivate a purchase
      description: |
        Does the same as reactivating a purchase in the DPD admin: emails the
        customer their purchase again. Only works on active purchases.
      parameters:
        - $ref: '#/components/parameters/id'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                buyer_email:
                  type: string
                  format: email
                  description: |
                    Send to this address instead, and change the purchase's
                    email address to it. Leave it off to use the address on
                    the purchase.
                refulfill:
                  type: boolean
                  description: Also send the purchase to its fulfillment service again.
      responses:
        '200':
          description: The purchase was reactivated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    const: OK
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: The purchase isn't active, or `buyer_email` isn't a valid address.
          content:
            application/json:
              schema:
                type: object
                properties:
                  errors:
                    type: object
                    additionalProperties:
                      type: string
              example:
                errors:
                  status: Purchase is not active

  /customers:
    get:
      tags: [Customers]
      operationId: listCustomers
      summary: List customers
      parameters:
        - name: email
          in: query
          description: Matches the start of the address, not case sensitive.
          schema:
            type: string
        - name: first_name
          in: query
          description: Matches the start of the name, not case sensitive.
          schema:
            type: string
        - name: last_name
          in: query
          description: Matches the start of the name, not case sensitive.
          schema:
            type: string
        - name: product_id
          in: query
          description: Only customers who bought this product.
          schema:
            type: integer
        - name: receives_newsletters
          in: query
          description: Only customers who do (or don't) get your product update emails.
          schema:
            type: boolean
        - $ref: '#/components/parameters/date_min'
        - $ref: '#/components/parameters/date_max'
        - $ref: '#/components/parameters/page'
      responses:
        '200':
          description: The matching customers.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Customer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /customers/{id}:
    get:
      tags: [Customers]
      operationId: getCustomer
      summary: Get a customer
      parameters:
        - $ref: '#/components/parameters/id'
      responses:
        '200':
          description: The customer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /storefronts/{storefront_id}/subscribers:
    get:
      tags: [Subscribers]
      operationId: listSubscribers
      summary: List subscribers
      parameters:
        - $ref: '#/components/parameters/storefront_id'
        - name: username
          in: query
          description: The subscriber's username (their email address).
          schema:
            type: string
        - $ref: '#/components/parameters/page'
      responses:
        '200':
          description: The storefront's subscribers.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Subscriber'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /storefronts/{storefront_id}/subscribers/verify:
    get:
      tags: [Subscribers]
      operationId: verifySubscriber
      summary: Verify a subscriber
      description: |
        Returns just the subscriber's subscription status, as plain text. Pass
        `id` or `username`. Subscribers with `ACTIVE`, `TRIAL` or `PAST_DUE`
        should get service.
      parameters:
        - $ref: '#/components/parameters/storefront_id'
        - name: username
          in: query
          description: The subscriber's username (their email address).
          schema:
            type: string
          example: test@example.com
        - name: id
          in: query
          description: The subscriber ID. Used instead of `username` when both are given.
          schema:
            type: integer
      responses:
        '200':
          description: The subscription status.
          content:
            text/plain:
              schema:
                $ref: '#/components/schemas/SubscriptionStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /storefronts/{storefront_id}/subscribers/{id}:
    get:
      tags: [Subscribers]
      operationId: getSubscriber
      summary: Get a subscriber
      parameters:
        - $ref: '#/components/parameters/storefront_id'
        - $ref: '#/components/parameters/id'
      responses:
        '200':
          description: The subscriber.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Subscriber'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

components:
  securitySchemes:
    basicAuth:
      type: http
      scheme: basic
      description: Your DPD username, and the API password from your profile in the DPD admin.

  parameters:
    id:
      name: id
      in: path
      required: true
      schema:
        type: integer
    storefront_id:
      name: storefront_id
      in: path
      required: true
      schema:
        type: integer
    page:
      name: page
      in: query
      description: Page number, 100 records per page. Leave it off to try to get every record.
      schema:
        type: integer
        minimum: 1
    date_min:
      name: date_min
      in: query
      description: Created after this date (anything PHP's `strtotime()` accepts).
      schema:
        type: string
        examples: ['2024-01-01']
    date_max:
      name: date_max
      in: query
      description: Created before this date (anything PHP's `strtotime()` accepts).
      schema:
        type: string
        examples: ['2024-12-31']

  responses:
    Unauthorized:
      description: The username or API password is wrong.
    NotFound:
      description: |
        Nothing found: the record doesn't exist or isn't yours, the list is
        empty, or the page is past the last one.
      content:
        application/json:
          schema:
            type: object
            properties:
              status:
                type: string
                const: NOTFOUND

  schemas:
    Timestamp:
      type: integer
      description: UNIX timestamp (seconds).
    Money:
      type: string
      examples: ['19.99']

    Storefront:
      type: object
      properties:
        id:
          type: integer
        created_at:
          $ref: '#/components/schemas/Timestamp'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
        name:
          type: string
        url:
          type: string
        contact_name:
          type: string
        contact_email:
          type: string
        currency:
          type: string
        type:
          type: string
          enum: [cart, subscription, clickbank]
        subdomain:
          type: string
          description: The store's subdomain. Add `.dpdcart.com` for its cart host.

    Product:
      type: object
      properties:
        id:
          type: integer
        created_at:
          $ref: '#/components/schemas/Timestamp'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
        storefront_id:
          type: integer
        name:
          type: string
        description:
          type: string
          description: Short description.
        long_description:
          type: string
        price:
          $ref: '#/components/schemas/Money'
        sku:
          type: string
        image_file_name:
          type: string
          description: The image's file name, for building an image URL.
        image_updated_at:
          $ref: '#/components/schemas/Timestamp'
        mime_type:
          type: string
        file_size:
          type: integer
        file_name:
          type: string
        prices:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
              name:
                type: string
              price:
                $ref: '#/components/schemas/Money'
        weight:
          type: integer
        visibility:
          type: integer
          enum: [0, 1]
          description: 0 is hidden, 1 is displayed.

    PurchaseStatus:
      type: string
      description: |
        - `ACT` Active: completed and paid.
        - `PND` Pending payment.
        - `RFD` Refunded, in part or in full.
        - `ERR` Error verifying the payment.
        - `CAN` Canceled.
        - `HLD` Held by fraud or blacklist checks.
      enum: [ACT, PND, RFD, ERR, CAN, HLD]

    Customer:
      type: object
      properties:
        id:
          type: integer
        firstname:
          type: string
        lastname:
          type: string
        email:
          type: string
        receives_email:
          type: boolean
          description: Whether the customer gets your product update emails.
        created_at:
          $ref: '#/components/schemas/Timestamp'
        updated_at:
          $ref: '#/components/schemas/Timestamp'

    Purchase:
      type: object
      properties:
        id:
          type: integer
        created_at:
          $ref: '#/components/schemas/Timestamp'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
        storefront_id:
          type: integer
        salt:
          type: string
          description: Unique hash code.
        status:
          $ref: '#/components/schemas/PurchaseStatus'
        currency:
          type: string
        subtotal:
          $ref: '#/components/schemas/Money'
        discount:
          $ref: '#/components/schemas/Money'
        tax:
          $ref: '#/components/schemas/Money'
        shipping:
          $ref: '#/components/schemas/Money'
        total:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: subtotal + discount + tax + shipping
        processor_fee:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: The payment processor's fee. Not recorded for every processor, and not part of `total`.
        buyer_email:
          type: string
        buyer_firstname:
          type: string
        buyer_lastname:
          type: string
        customer:
          $ref: '#/components/schemas/Customer'
        customer_notes:
          type: string
        custom_fields:
          type: array
          items:
            type: object
            properties:
              label:
                type: string
              response:
                type: string
        shipping_firstname:
          type: string
        shipping_lastname:
          type: string
        shipping_street:
          type: string
        shipping_street2:
          type: string
        shipping_city:
          type: string
        shipping_state:
          type: string
          description: State or province code.
        shipping_zip_code:
          type: string
        shipping_country_code:
          type: string
        shipping_country:
          type: string
          description: Full country name.
        shipping_phone:
          type: string
        shipping_vatin:
          type: string
          description: VAT identification number.
        shipping_business_name:
          type: string
        shipping_carrier:
          type: string
          description: UPS, FedEx and so on.
        shipping_tracking_number:
          type: string
        requires_shipping:
          type: boolean
        tangibles_to_ship:
          type: [integer, 'null']
          description: Goods not shipped yet. 0 when everything has shipped, null when nothing needed shipping.
        billing_firstname:
          type: string
        billing_lastname:
          type: string
        billing_street:
          type: string
        billing_street2:
          type: string
        billing_city:
          type: string
        billing_state:
          type: string
        billing_zip_code:
          type: string
        billing_country_code:
          type: string
        billing_card_type:
          type: string
        billing_card_number:
          type: string
          description: The last digits of the card, padded with asterisks.
        line_items:
          type: array
          items:
            $ref: '#/components/schemas/LineItem'
        coupons:
          type: array
          items:
            $ref: '#/components/schemas/Coupon'
        ip_address:
          type: string
          description: The IP address the customer checked out from.
        marketing_optin:
          type: boolean
          description: Whether the customer opted in to marketing emails.
        transactions:
          type: array
          items:
            $ref: '#/components/schemas/Transaction'

    LineItem:
      type: object
      properties:
        id:
          type: integer
        purchase_id:
          type: integer
        product_id:
          type: integer
        price:
          $ref: '#/components/schemas/Money'
        quantity:
          type: integer
        subtotal:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: price × quantity
        tax:
          $ref: '#/components/schemas/Money'
        discount:
          $ref: '#/components/schemas/Money'
        total:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: subtotal + tax + discount
        product_name:
          type: string
        download_limit:
          type: integer
          description: How many times it may be downloaded. 0 is unlimited.
        download_count:
          type: integer
        expires_at:
          $ref: '#/components/schemas/Timestamp'
        product_keys:
          type: array
          description: The product keys delivered with this line item.
          items:
            type: string
        has_all_keys:
          type: boolean
          description: Whether every key was generated.

    Coupon:
      type: object
      properties:
        name:
          type: string
        code:
          type: string
        discount_amount:
          type: string
          description: A percentage or an amount of money, depending on `discount_type`.
        discount_type:
          type: string
          enum: [money, percent]

    Transaction:
      type: object
      properties:
        id:
          type: integer
        purchase_id:
          type: integer
        action:
          type: string
          description: Depends on the gateway. Usually `AuthorizeAndCapture`.
        amount:
          $ref: '#/components/schemas/Money'
        currency:
          type: string
          description: ISO 4217 currency code.
        gateway:
          type: string
        is_success:
          type: boolean
        reference:
          type: string
          description: The gateway's reference, such as a Stripe charge ID.
        message:
          type: string
          description: The gateway's response message, such as "Approved".
        created_at:
          $ref: '#/components/schemas/Timestamp'
        updated_at:
          $ref: '#/components/schemas/Timestamp'

    SubscriptionStatus:
      type: string
      description: |
        - `NEW` Not activated yet: checking out, or abandoned signup.
        - `TRIAL` In the trial period.
        - `ACTIVE` Active, no payment due.
        - `CANCELED` Canceled, but the current term hasn't ended.
        - `CLOSED` Canceled and closed.
        - `PAST_DUE` Active, but a payment is overdue. Closes if it isn't paid.
      enum: [NEW, TRIAL, ACTIVE, CANCELED, CLOSED, PAST_DUE]

    Subscriber:
      type: object
      properties:
        id:
          type: integer
        created_at:
          $ref: '#/components/schemas/Timestamp'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
        username:
          type: string
          description: The subscriber's email address.
        last_login_at:
          $ref: '#/components/schemas/Timestamp'
        subscription:
          $ref: '#/components/schemas/Subscription'

    Subscription:
      type: object
      properties:
        id:
          type: integer
        status:
          $ref: '#/components/schemas/SubscriptionStatus'
        price:
          $ref: '#/components/schemas/Money'
        period:
          type: integer
        unit:
          type: string
          description: The billing period's unit, one character.
        tax_amount:
          $ref: '#/components/schemas/Money'
        trial_price:
          $ref: '#/components/schemas/Money'
        trial_period:
          type: integer
        trial_unit:
          type: string
        trial_tax_amount:
          $ref: '#/components/schemas/Money'
        created_at:
          $ref: '#/components/schemas/Timestamp'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
        started_at:
          $ref: '#/components/schemas/Timestamp'
        ended_at:
          $ref: '#/components/schemas/Timestamp'
        trial_started_at:
          $ref: '#/components/schemas/Timestamp'
        last_payment_at:
          $ref: '#/components/schemas/Timestamp'
        next_payment_at:
          $ref: '#/components/schemas/Timestamp'
