openapi: 3.0.3
info:
  title: Drop Privacy Screening API
  version: "0.8.56"
  description: >
    Submit a list of people and find out how many — and which — have filed a California DROP
    deletion request, matched against the cumulative DROP request registry. Available to registered
    data brokers and standalone screening consumers alike; authenticate with a screening key we
    issue. No PII is stored (identifiers are hashed in memory and discarded; only match counts are logged).


    **Getting a key:** to try the API, email sales@dropprivacy.com for a screening key (you'll receive
    a key and your tenant slug). A sandbox key (`dropk_test_…`) screens the SANDBOX test dataset — see
    the identifiers under `x-sandbox-test-dataset` for guaranteed matches.
  contact:
    name: Drop Privacy
    url: https://dropprivacy.com
  license:
    name: Proprietary

servers:
  # Same origin as the page serving these docs — use this for "Try it out" so the browser makes a
  # same-origin request (no CORS). On https://dropprivacy.com/developers this resolves to the live API.
  - url: /api/v1
    description: This host (same origin — use for Try it out)
  # Absolute URL for Postman / other clients (a browser calling this cross-origin will be blocked by CORS).
  - url: https://dropprivacy.com/api/v1
    description: Production (absolute)

security:
  - ApiKeyAuth: []

tags:
  - name: Screening
    description: Screen PII against the DROP deletion registry.

paths:
  /tenant/{slug}/screen:
    post:
      tags: [Screening]
      summary: Screen a batch of people
      description: >
        Screen a list of records. Batches at or below the sync cap (default 5,000) answer
        synchronously with counts (and, for `output: per_record`, per-record match flags). Larger
        batches return `202 Accepted` with a job id to poll. A person matches if ANY of their
        identifiers (name+DOB+ZIP, name+VIN, email, phone, MAID, or CTVID) is a deletion request.
      operationId: screenBatch
      parameters:
        - $ref: '#/components/parameters/Slug'
        - name: environment
          in: query
          required: false
          description: >
            Optional. Registry scope. Defaults to your key's environment (test key → SANDBOX, live key
            → PROD). For JSON you can also set it in the request body.
          schema:
            type: string
            enum: [SANDBOX, PROD]
        - name: output
          in: query
          required: false
          description: Optional. `summary` returns counts only. For JSON you can also set it in the body.
          schema:
            type: string
            enum: [per_record, summary]
            default: per_record
        - name: format
          in: query
          required: false
          description: Optional. Output serialization; overrides the `Accept` header.
          schema:
            type: string
            enum: [json, csv, ndjson]
            default: json
        - name: Idempotency-Key
          in: header
          required: false
          description: >
            Optional. On a large (async) submit, a retry with the same key returns the existing job
            instead of starting a second one.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScreenRequest'
            examples:
              sandboxAll:
                summary: "Sandbox — all 18 test records (expect matched=18)"
                description: >
                  Picks the SANDBOX environment and every sandbox test consumer, so it matches on all
                  six list types. Use with the sandbox key.
                value:
                  environment: SANDBOX
                  output: per_record
                  records:
                    - { ref: "s1",  first_name: "Michael",   last_name: "Smith",     dob: "03/21/1973", zip: "90117" }
                    - { ref: "s2",  first_name: "James",     last_name: "Armstrong", dob: "Sep 12 1939", zip: "93114" }
                    - { ref: "s3",  first_name: "Abigail",   last_name: "Hawthorne", dob: "01/30/1980", zip: "93712" }
                    - { ref: "s4",  first_name: "León",      last_name: "Navarro",   dob: "1982-05-08", zip: "92936" }
                    - { ref: "s5",  first_name: "Ella Jane", last_name: "Smith",     dob: "1946-03-12", zip: "91546" }
                    - { ref: "s6",  first_name: "Elijah",    last_name: "Hawthorne", vin: "AC1PHVASYM1R02CKK" }
                    - { ref: "s7",  first_name: "Ari",       last_name: "Armstrong", vin: "CLVR5WUUE8LRUZ4YN" }
                    - { ref: "s8",  email: "john.clark@mail.test.net" }
                    - { ref: "s9",  email: "charlottefitzgerald@example.com" }
                    - { ref: "s10", email: "user+one@example.com" }
                    - { ref: "s11", email: "lilyannewhite@example.com" }
                    - { ref: "s12", phone: "+1 555 193 0492" }
                    - { ref: "s13", phone: "1-555-130-3065" }
                    - { ref: "s14", phone: "(555) 135-4470" }
                    - { ref: "s15", maid: "9201F635-25EE-4BBB-9473-5F3E5ECAE0B7" }
                    - { ref: "s16", maid: "19BB3716-FDB0-41BA-8B60-E03D8B8E0833" }
                    - { ref: "s17", ctvid: "nova_PK9-H3ND:NRW4P" }
                    - { ref: "s18", ctvid: "zephyr_Z4M-H83V:VH5AR" }
              minimal:
                summary: Minimal (defaults env to your key, output to per_record)
                value:
                  records:
                    - { ref: "1001", email: "john.clark@mail.test.net" }
                    - { ref: "1002", email: "nobody@example.com" }
              fullPii:
                summary: Full PII with aliased/mixed-case field names
                value:
                  environment: SANDBOX
                  output: per_record
                  records:
                    - ref: "p1"
                      First Name: "Michael"
                      Last Name: "Smith"
                      DOB: "03/21/1973"
                      ZIP: "90117"
                      Email_Address: "michael.smith@example.com"
                      Cell: "(555) 010-2000"
                    - ref: "p2"
                      first: "Elijah"
                      last: "Hawthorne"
                      vin: "AC1PHVASYM1R02CKK"
              fieldMap:
                summary: Custom column names via field_map
                value:
                  field_map: { customer_email_1: "email" }
                  records:
                    - { ref: "x1", customer_email_1: "user+one@example.com" }
          text/csv:
            schema:
              type: string
            example: |
              ref,First Name,Last Name,DOB,ZIP
              p1,Michael,Smith,03/21/1973,90117
          application/x-ndjson:
            schema:
              type: string
            example: |
              {"ref":"1","email":"john.clark@mail.test.net"}
              {"ref":"2","phone":"+1 555 193 0492"}
      responses:
        '200':
          description: Synchronous screening result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScreenResult'
              examples:
                perRecord:
                  value:
                    screen_id: "scr_9f3a1c7d2e4b0a11"
                    environment: SANDBOX
                    counts:
                      submitted: 2
                      matched: 1
                      invalid: 0
                      match_rate: 0.5
                      by_list_type: { EMAIL: 1 }
                    records:
                      - { ref: "1001", matched: true, matched_list_types: ["EMAIL"], last_requested_at: "2026-07-09" }
                      - { ref: "1002", matched: false }
            text/csv:
              schema:
                type: string
              example: |
                ref,matched,matched_list_types,last_requested_at
                1001,true,EMAIL,2026-07-09
                1002,false,,
            application/x-ndjson:
              schema:
                type: string
        '202':
          description: Batch exceeded the sync cap — accepted as an async job to poll.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AsyncAccepted'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /tenant/{slug}/screen/{id}:
    get:
      tags: [Screening]
      summary: Poll an async screen
      operationId: getScreen
      parameters:
        - $ref: '#/components/parameters/Slug'
        - $ref: '#/components/parameters/ScreenId'
      responses:
        '200':
          description: Current status (and counts when done).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScreenStatus'
              examples:
                queued:
                  value: { screen_id: "scr_1234", status: "queued", progress: { done: 40000, total: 120000 } }
                done:
                  value:
                    screen_id: "scr_1234"
                    status: "done"
                    counts: { submitted: 120000, matched: 318, invalid: 0, match_rate: 0.00265, by_list_type: { EMAIL: 210, PHONE: 108 } }
                    download_url: "/api/v1/tenant/sample-api-screen/screen/1234/download"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /tenant/{slug}/screen/{id}/download:
    get:
      tags: [Screening]
      summary: Download an async result
      operationId: downloadScreen
      parameters:
        - $ref: '#/components/parameters/Slug'
        - $ref: '#/components/parameters/ScreenId'
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum: [json, csv, ndjson]
      responses:
        '200':
          description: The full result in the requested format.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScreenResult'
            text/csv:
              schema: { type: string }
            application/x-ndjson:
              schema: { type: string }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Result not ready yet (job still running).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: >
        Screening key, `dropk_live_…` (production) or `dropk_test_…` (sandbox). The prefix determines
        the default `environment`.

  parameters:
    Slug:
      name: slug
      in: path
      required: true
      description: Your tenant slug (e.g. `sample-api-screen`).
      schema:
        type: string
    ScreenId:
      name: id
      in: path
      required: true
      description: The async screen's numeric job id (from the `202` `screen_id`, minus the `scr_` prefix).
      schema:
        type: integer
        format: int64

  schemas:
    ScreenRequest:
      type: object
      required: [records]
      properties:
        environment:
          type: string
          enum: [SANDBOX, PROD]
          description: Optional. Defaults to your key's environment (`dropk_live_`→PROD, `dropk_test_`→SANDBOX).
        output:
          type: string
          enum: [per_record, summary]
          default: per_record
          description: Optional. `per_record` returns which records matched; `summary` returns counts only. May be capped to `summary` per key.
        field_map:
          type: object
          additionalProperties:
            type: string
          description: Optional per-request map of your column names to canonical fields (e.g. map `cust_email_1` to `email`).
        records:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/Record'
      example:
        output: per_record
        records:
          - { ref: "1001", first_name: "Jane", last_name: "Doe", dob: "1970-01-02", zip: "90001", email: "jane@example.com", phone: "555-123-4567" }

    Record:
      type: object
      description: >
        A person. Supply any subset of identifiers. Field keys are matched case- and
        separator-insensitively plus synonyms (FirstName/FIRST_NAME/fname → first_name;
        Email_Address/mail → email; Cell/mobile → phone; IDFA/GAID → maid; CTV_ID → ctvid).
        Unrecognized columns are ignored. Only one value per identifier type is screened per record.
      properties:
        ref:
          type: string
          description: Your own row id, echoed back. Do NOT put PII here.
        first_name: { type: string }
        last_name: { type: string }
        dob: { type: string, description: "Any common format (YYYY-MM-DD, MM/DD/YYYY, 'Sep 12 1939')." }
        zip: { type: string }
        vin: { type: string }
        email: { type: string }
        phone: { type: string }
        maid: { type: string, description: "Mobile advertising id (IDFA/GAID)." }
        ctvid: { type: string, description: "Connected-TV id." }
      additionalProperties: true
      example: { ref: "1002", email: "bob@example.com" }

    ScreenResult:
      type: object
      properties:
        screen_id:
          type: string
          example: "scr_9f3a1c7d2e4b0a11"
        environment:
          type: string
          enum: [SANDBOX, PROD]
        counts:
          $ref: '#/components/schemas/Counts'
        records:
          type: array
          description: Present only when output mode is per_record.
          items:
            $ref: '#/components/schemas/ResultRecord'

    Counts:
      type: object
      properties:
        submitted: { type: integer, example: 75 }
        matched: { type: integer, example: 18 }
        invalid: { type: integer, description: "Records rejected (e.g. strict mode, unmapped field).", example: 0 }
        match_rate: { type: number, format: float, example: 0.24 }
        by_list_type:
          type: object
          additionalProperties: { type: integer }
          description: Matched people per list type.
          example: { NDZ: 5, NVIN: 2, EMAIL: 4, PHONE: 3, MAID: 2, CTVID: 2 }

    ResultRecord:
      type: object
      properties:
        ref: { type: string }
        matched: { type: boolean }
        matched_list_types:
          type: array
          items:
            type: string
            enum: [NDZ, NVIN, EMAIL, PHONE, MAID, CTVID]
        last_requested_at:
          type: string
          nullable: true
          description: Date DROP last published this deletion request (YYYY-MM-DD).
        error:
          type: string
          description: Present if the record was invalid (e.g. unmapped field in strict mode).
        unmapped:
          type: array
          items: { type: string }
      example: { ref: "1002", matched: true, matched_list_types: ["EMAIL"], last_requested_at: "2026-07-09" }

    AsyncAccepted:
      type: object
      properties:
        screen_id: { type: string, example: "scr_1234" }
        status: { type: string, enum: [queued], example: "queued" }
        poll_url: { type: string, example: "/api/v1/tenant/sample-api-screen/screen/1234" }
        idempotent_replay:
          type: boolean
          description: True if this returned an existing job for a repeated Idempotency-Key.

    ScreenStatus:
      type: object
      properties:
        screen_id: { type: string }
        status: { type: string, enum: [queued, done, failed] }
        counts:
          $ref: '#/components/schemas/Counts'
        progress:
          type: object
          properties:
            done: { type: integer, nullable: true }
            total: { type: integer, nullable: true }
        download_url: { type: string }
        error: { type: string }

    Error:
      type: object
      properties:
        error:
          type: string
          example: "Invalid API key"

  responses:
    BadRequest:
      description: Malformed body, unparseable format, or no records.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing/invalid X-API-Key (or unknown tenant — 401, never 404).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Screening is not enabled for this key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Unknown screen id, or the screening API is disabled.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Rate limit exceeded.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds to wait before retrying.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

x-sandbox-test-dataset:
  description: >
    Real DROP sandbox deletion requests loaded in the SANDBOX registry. Submit these (with the sandbox
    key) for guaranteed matches. Each matches on one list type.
  records:
    - { list_type: NDZ, first_name: Michael, last_name: Smith, dob: "03/21/1973", zip: "90117" }
    - { list_type: NDZ, first_name: James, last_name: Armstrong, dob: "Sep 12 1939", zip: "93114" }
    - { list_type: NDZ, first_name: Abigail, last_name: Hawthorne, dob: "01/30/1980", zip: "93712" }
    - { list_type: NDZ, first_name: León, last_name: Navarro, dob: "1982-05-08", zip: "92936" }
    - { list_type: NDZ, first_name: "Ella Jane", last_name: Smith, dob: "1946-03-12", zip: "91546" }
    - { list_type: NVIN, first_name: Elijah, last_name: Hawthorne, vin: "AC1PHVASYM1R02CKK" }
    - { list_type: NVIN, first_name: Ari, last_name: Armstrong, vin: "CLVR5WUUE8LRUZ4YN" }
    - { list_type: EMAIL, email: "john.clark@mail.test.net" }
    - { list_type: EMAIL, email: "charlottefitzgerald@example.com" }
    - { list_type: EMAIL, email: "user+one@example.com" }
    - { list_type: EMAIL, email: "lilyannewhite@example.com" }
    - { list_type: PHONE, phone: "+1 555 193 0492" }
    - { list_type: PHONE, phone: "1-555-130-3065" }
    - { list_type: PHONE, phone: "(555) 135-4470" }
    - { list_type: MAID, maid: "9201F635-25EE-4BBB-9473-5F3E5ECAE0B7" }
    - { list_type: MAID, maid: "19BB3716-FDB0-41BA-8B60-E03D8B8E0833" }
    - { list_type: CTVID, ctvid: "nova_PK9-H3ND:NRW4P" }
    - { list_type: CTVID, ctvid: "zephyr_Z4M-H83V:VH5AR" }
