openapi: 3.1.0
info:
  title: DROP Data Broker API
  version: "1.0.0"
  summary: California Delete Act — Data Broker Integration API
  description: |
    The DROP (Delete Request and Opt-out Platform) Data Broker API enables
    data brokers to integrate with DROP in order to programmatically
    retrieveing consumer deletion request data and reporting status updates.

    ## How It Works

    1. **Download** — Retrieve a ZIP archive of CSV files containing hashed consumer identifiers.
    2. **Process** — Match hashed identifiers against your records and take the required action (delete, opt out, exempt, or not found).
    3. **Upload** — Submit a CSV response file reporting the status for each work item.

    ## Authentication

    All requests require an API key passed in the `X-API-KEY` header. API keys are
    generated in the [Data Broker Portal](https://databroker.drop.privacy.ca.gov/Dashboard)
    after registration and fee payment are complete.

    ## Daily Maintenance Window

    Push and pull requests are closed between **1:00 AM – 3:00 AM Pacific Time** daily
    while the platform batches the most current data. Requests during this window may
    return errors and should be retried afterward.

    ## Normalization and Hashing

    All identifiers are hashed with **SHA-256** using UTF-8 input encoding.
    The output is **Base64**.

    ### Canonicalization Rules

    The following canonicalization rules are applied before hashing.
    These rules are normative for interoperability with the sandbox CSV files and submission summary reports.

    - Email: remove all whitespace, then lowercase.
    - DOB: format as `YYYYMMDD` with no separators.
    - Phone: keep digits only, then retain the last 10 digits.
    - ZIP: keep alphanumeric characters only, lowercase, truncate to the first 5 characters, then remove leading zeros.
    - Names: transliterate to ASCII, lowercase, then remove spaces, punctuation, separators, and any remaining non-alphanumeric characters.
      Examples: `Lily-Anne -> lilyanne`, `D'Amico -> damico`, `Ella Jane -> ellajane`, `Nguyễn -> nguyen`, `Михаил -> mikhail`.
    - MAID: lowercase, then remove hyphens and any other non-alphanumeric characters.
    - VIN: lowercase, then remove any non-alphanumeric characters.
    - CTVID: lowercase, then remove punctuation and separators such as `_`, `-`, and `:`.

    ### NDZ and NVIN Concatenated Hashes

    For concatenated list types, each normalized field is hashed independently to Base64,
    concatenates those Base64 digests with no separator, then hashes the concatenated UTF-8 string again
    and Base64-encodes the final digest.

    - NDZ uses `firstName + lastName + dob + zip`
    - NVIN uses `firstName + lastName + vin`

    C# reference implementation:

    ```csharp
    using System.Security.Cryptography;
    using System.Text;
    using System.Text.RegularExpressions;

    static string ComputeSha256Base64(string canonical)
    {
        using var sha = SHA256.Create();
        var hashBytes = sha.ComputeHash(Encoding.UTF8.GetBytes(canonical));
      return Convert.ToBase64String(hashBytes);
    }

    static string NormalizeEmail(string input)
        => Regex.Replace(input ?? "", @"\s+", "").ToLowerInvariant();

    static string NormalizePhone(string inputNumber)
    {
        var digits = new string((inputNumber ?? "").Where(char.IsDigit).ToArray());
        return digits.Length > 10 ? digits[^10..] : digits;
    }

    static string NormalizeZip(string input)
    {
        var cleaned = new string((input ?? "").Where(char.IsLetterOrDigit).ToArray()).ToLowerInvariant();
      var truncated = cleaned.Length <= 5 ? cleaned : cleaned[..5];
      return truncated.TrimStart('0');
    }
    ```

    Names are normalized per field (first name and last name independently)
    before any list-specific concatenation workflow.

servers:
  - url: https://api.drop.privacy.ca.gov
    description: Production
  - url: https://api.drop.privacy.ca.gov/sandbox
    description: Sandbox

externalDocs:
  description: Human-readable integration guide
  url: ./docs.html

tags:
  - name: Download
    description: Retrieve consumer deletion request data files
  - name: Upload
    description: Submit status response files

x-tagGroups:
  - name: Core workflow
    tags:
      - Download
      - Upload

security:
  - ApiKeyAuth: []

paths:
  /data/download:
    get:
      operationId: downloadData
      summary: Download consumer deletion request data
      description: |
        Returns a ZIP archive containing one CSV file per consumer data
        list enabled for the authenticated API key.

        Each CSV uses the naming convention:
        ```
        <YYYYMMDD>_<DataBrokerId>_<DataType>.csv
        ```

        **CSV schemas vary by list type:**

        | List Type | Columns |
        |-----------|---------|
        | NDZ       | `Id`, `Hash` |
        | EMAIL     | `Id`, `Hash` |
        | PHONE     | `Id`, `Hash` |
        | MAID      | `Id`, `Hash` |
        | NVIN      | `Id`, `Hash` |
        | CTVID     | `Id`, `Hash` |

        Filename capitalization is not significant. Examples below use mixed-case file names.

        > **Important:** Preserve the exact file name when uploading your response.
      tags:
        - Download
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X GET "https://api.drop.privacy.ca.gov/data/download" \
              -H "accept: */*" \
              -H "X-API-KEY: your-api-key-here" \
              --output download.zip
        - lang: PowerShell
          label: Invoke-WebRequest
          source: |
            $headers = @{
              "X-API-KEY" = "your-api-key-here"
              "Accept" = "text/zip"
            }
            Invoke-WebRequest \
              -Uri "https://api.drop.privacy.ca.gov/data/download" \
              -Headers $headers \
              -OutFile "download.zip"
      security:
        - ApiKeyAuth: []
      responses:
        "200":
          description: ZIP archive containing CSV files for each enabled consumer data list.
          content:
            application/zip:
              schema:
                type: string
                format: binary
              examples:
                archiveContents:
                  $ref: "#/components/examples/DownloadArchiveContents"
          headers:
            Content-Disposition:
              schema:
                type: string
              example: attachment; filename="download.zip"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/ServerError"

  /data/upload:
    post:
      operationId: uploadData
      summary: Upload new status response file
      description: |
        Upload a CSV response file for the **first time** for a given downloaded file.

        One or more CSV response files may be uploaded in the same request.

        Each CSV must:
        - Use the header `Id,Status`
        - Use the original downloaded file name, optionally with an added suffix before `.csv`
        - Use an optional suffix of up to 10 characters when needed to distinguish repeated uploads for the same base file name
        - Contain valid status codes (2, 3, 4, or 5) for each work item ID
        - Be uploaded as a CSV file, not a ZIP archive

        The files are sent as `multipart/form-data` with the field name `files`.

        Valid file name examples:
        - `20260312_4821_Email.csv`
        - `20260312_4821_Email_part01.csv`
      tags:
        - Upload
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.drop.privacy.ca.gov/data/upload" \
              -H "accept: */*" \
              -H "X-API-KEY: your-api-key-here" \
              -H "Content-Type: multipart/form-data" \
              -F "files=@20260312_4821_Email.csv;type=text/csv"
        - lang: JavaScript
          label: fetch + FormData
          source: |
            const formData = new FormData();
            formData.append('files', fileBlob, '20260312_4821_Email.csv');

            const response = await fetch(
              'https://api.drop.privacy.ca.gov/data/upload',
              {
                method: 'POST',
                headers: {
                  'X-API-KEY': 'your-api-key-here'
                },
                body: formData
              }
            );

            const result = await response.json();
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - files
              properties:
                files:
                  type: array
                  items:
                    type: string
                    format: binary
                  description: |
                    One or more CSV response files. Each file must use the original
                    downloaded archive name or that name plus an optional suffix of up to
                    10 characters before `.csv`, and must have `Id,Status` as the header row.
            examples:
              uploadEmail:
                $ref: "#/components/examples/UploadEmailFile"
      responses:
        "200":
          description: Upload processed. Check `acceptedCount` and `rejectedCount` to determine outcome.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadResponse"
              examples:
                accepted:
                  $ref: "#/components/examples/UploadAcceptedResponse"
                invalidHeader:
                  $ref: "#/components/examples/UploadInvalidHeaderResponse"
                duplicateFile:
                  $ref: "#/components/examples/UploadDuplicateFileResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/ServerError"

  /data/amend:
    post:
      operationId: uploadAmend
      summary: Amend a previously uploaded status response file
      description: |
        Upload a corrected CSV response file for a previously submitted download.

        One or more corrected CSV response files may be uploaded in the same request.

        The request format is identical to `/data/upload`. Use this endpoint when you
        need to update statuses in a file that was already accepted via `/data/upload`.
      tags:
        - Upload
      x-codeSamples:
        - lang: Shell
          label: curl
          source: |
            curl -X POST "https://api.drop.privacy.ca.gov/data/amend" \
              -H "accept: */*" \
              -H "X-API-KEY: your-api-key-here" \
              -H "Content-Type: multipart/form-data" \
              -F "files=@20260312_4821_Email.csv;type=text/csv"
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - files
              properties:
                files:
                  type: array
                  items:
                    type: string
                    format: binary
                  description: |
                    One or more amended CSV response files. Same naming and schema
                    requirements as the `/data/upload` endpoint.
            examples:
              amendEmail:
                $ref: "#/components/examples/UploadEmailFile"
      responses:
        "200":
          description: Amendment processed. Check `acceptedCount` and `rejectedCount` to determine outcome.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadResponse"
              examples:
                amendAccepted:
                  $ref: "#/components/examples/UploadAmendAcceptedResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/ServerError"

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY
      description: |
        API key issued through the Data Broker Portal after completing DROP
        registration and fee payment. The key grants access only to the
        consumer data lists selected during setup.

  schemas:
    UploadMode:
      type: string
      enum:
        - new
        - amend
      description: Upload behavior selector represented by the endpoint used.

    ListType:
      type: string
      enum:
        - NDZ
        - EMAIL
        - PHONE
        - MAID
        - NVIN
        - CTVID
      description: Consumer data list type enabled for the API key. These values match the live sandbox file suffixes.

    CtvIdValue:
      type: string
      minLength: 8
      maxLength: 32
      description: |
        Connected TV identifier value.
        Canonicalization is lowercase plus removal of punctuation and separators.
        Canonical values that start with `ext` are treated as non-matchable in the live sandbox.

    FileNamingConvention:
      type: object
      description: Structure embedded in every DROP CSV file name.
      properties:
        YYYYMMDD:
          type: string
          pattern: "^[0-9]{8}$"
          description: File date.
        DataBrokerId:
          type: string
          pattern: "^[0-9]{4}$"
          description: Short broker identifier, currently 4 digits.
        DataType:
          $ref: "#/components/schemas/ListType"
        OptionalSuffix:
          type: string
          maxLength: 10
          description: |
            Optional suffix appended before `.csv` when uploading or amending files
            that share the same base downloaded file name, for example `part01` in
            `20260312_4821_Email_part01.csv`.

    UploadResponse:
      type: object
      description: Response returned after a file upload (new or amend).
      required:
        - mode
        - acceptedCount
        - rejectedCount
        - accepted
        - rejected
      properties:
        mode:
          $ref: "#/components/schemas/UploadMode"
          description: The upload mode — `new` for first submission, `amend` for corrections.
          example: "new"
        acceptedCount:
          type: integer
          description: Number of files accepted in this request.
          example: 1
        rejectedCount:
          type: integer
          description: Number of files rejected in this request.
          example: 0
        accepted:
          type: array
          description: List of accepted files with confirmation messages.
          items:
            $ref: "#/components/schemas/FileResult"
        rejected:
          type: array
          description: List of rejected files with error messages.
          items:
            $ref: "#/components/schemas/FileResult"

    FileResult:
      type: object
      description: Result for a single file in an upload response.
      required:
        - fileName
        - message
      properties:
        fileName:
          type: string
          description: |
            The name of the uploaded CSV file.
            This must be the original downloaded file name or that name plus an optional
            suffix of up to 10 characters before `.csv`.
          example: "20260312_4821_Email.csv"
        message:
          type: string
          description: |
            Human-readable result message. Common values:
            - `Accepted. NEW file queued for processing.`
            - `Accepted. AMEND file queued for processing.`
            - `Invalid CSV header. Expected: Id,Status`
            - `You already uploaded a file with the same filename for this run.`
          example: "Accepted. NEW file queued for processing."

    StatusCode:
      type: integer
      description: |
        Status code reported for each work item in the uploaded CSV.
      enum:
        - 2
        - 3
        - 4
        - 5
      x-enum-descriptions:
        - "Record exempted — match found, data exempt under Civil Code §1798.99.86(c)(2)"
        - "Record deleted — match found, non-exempt personal information deleted"
        - "Record opted out — multiple consumers linked, all opted out of sale or sharing"
        - "Record not found — no match after matching process"

    DownloadedCSVRow:
      type: object
      description: |
        A single row from a downloaded CSV file.
        The live sandbox currently uses `Id,Hash` for all list types, including NDZ and NVIN.
        For NDZ and NVIN, the `Hash` value is the final concatenated-hash digest.
      properties:
        Id:
          type: integer
          description: The work item identifier used by the live sandbox.
          example: 679
        Hash:
          type: string
          description: |
            SHA-256 Base64 hash of the consumer identifier.
            For NDZ and NVIN, this contains the final concatenated-hash digest.
          example: "KA18MT/ph6IHYjzT9zwETySDQyvSh87YuoSBpOQtkhE="

    UploadCSVRow:
      type: object
      description: A single row in the response CSV uploaded by the data broker.
      required:
        - Id
        - Status
      properties:
        Id:
          type: integer
          description: The work item identifier from the downloaded file.
          example: 679
        Status:
          $ref: "#/components/schemas/StatusCode"

  responses:
    BadRequest:
      description: |
        Invalid parameters, file formatting, file name, or CSV columns.
        Correct the request before retrying.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
          example:
            error: "Bad Request — invalid parameters or file formatting."
    Unauthorized:
      description: |
        API key is missing or invalid. Generate a new key from the Data Broker Portal.
    Forbidden:
      description: |
        Access denied. This typically means payment has not been completed
        through the Data Broker Portal.
    NotFound:
      description: |
        The requested endpoint does not exist. Verify the API URL.
    TooManyRequests:
      description: |
        Rate limit exceeded. Rate limits are adjusted automatically based on
        system load. Back off and retry after a delay.
    ServerError:
      description: |
        Internal server error. This may occur during the daily maintenance
        window (1:00 AM – 3:00 AM Pacific Time) or due to a transient issue.
        Retry later.

  examples:
    DownloadArchiveContents:
      summary: ZIP archive contents
      description: The binary ZIP response usually contains one CSV per enabled consumer data list.
      value: |
        20260312_4821_NDZ.csv
        20260312_4821_Email.csv
        20260312_4821_Phone.csv
    DownloadEmailCsv:
      summary: Example downloaded Email CSV
      value: |
        Id,Hash
        679,KA18MT/ph6IHYjzT9zwETySDQyvSh87YuoSBpOQtkhE=
    DownloadNdzCsv:
      summary: Example downloaded NDZ CSV
      value: |
        Id,Hash
        679,PQOfn1RffEKmqMmNAzDKKaoZCwxWbQZkQzPWmQo9REA=
    UploadStatusCsv:
      summary: Example upload CSV
      value: |
        Id,Status
        679,2
        680,5
    UploadEmailFile:
      summary: Multipart upload example
      description: Upload one or more CSV response files using the original downloaded file name or that name plus an optional suffix.
      value:
        files:
          - "@20260312_4821_Email.csv"
          - "@20260312_4821_Email_part01.csv"
    UploadAcceptedResponse:
      summary: Accepted new upload
      description: DROP accepted the file and queued it for downstream processing.
      value:
        mode: "new"
        acceptedCount: 1
        rejectedCount: 0
        accepted:
          - fileName: "20260312_4821_Email.csv"
            message: "Accepted. NEW file queued for processing."
        rejected: []
    UploadInvalidHeaderResponse:
      summary: Rejected upload with invalid CSV header
      description: The upload reached DROP, but the CSV header was not exactly `Id,Status`.
      value:
        mode: "new"
        acceptedCount: 0
        rejectedCount: 1
        accepted: []
        rejected:
          - fileName: "20260312_4821_Email.csv"
            message: "Invalid CSV header. Expected: Id,Status"
    UploadDuplicateFileResponse:
      summary: Rejected duplicate upload
      description: The same file name was already submitted for this run. Use a unique optional suffix in the file name for another upload.
      value:
        mode: "new"
        acceptedCount: 0
        rejectedCount: 1
        accepted: []
        rejected:
          - fileName: "20260312_4821_Email.csv"
            message: "You already uploaded a file with the same filename for this run."
    UploadAmendAcceptedResponse:
      summary: Accepted amended upload
      description: DROP accepted the amended file and queued it for downstream processing.
      value:
        mode: "amend"
        acceptedCount: 1
        rejectedCount: 0
        accepted:
          - fileName: "20260312_4821_Email.csv"
            message: "Accepted. AMEND file queued for processing."
        rejected: []