Plumma CONNECT API

A single-operation aggregation API over mobile network operator intelligence. One POST /api call carries a phone number in E.164 form plus a `commands` array, and returns the requested signals in one response. The published enum carries 17 commands — line_classification, current_carrier, issuing_carrier, porting_timestamp, porting_logs, network_presence, roaming_intel, deactivation_point, churn_tracker, sim_swap, port_fraud_shield, digital_footprint, age_verification, kyc_match, divert_detector, commercial_segment and tenure_period. Authentication is a single API key header (x-plumma-connect-api-key, documented as a base64-encoded X.509 client certificate). Errors are RFC 7807 problem documents on every 4xx/5xx, carrying a correlation_id. The specification ships four worked request examples, including a multi-command call.

Operations 1

POST /api Run one or more intelligence commands against a phone number #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/plumma-connect-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

plumma-connect-openapi.yml Raw ↑
# Harvested verbatim from https://connect.plumma.it/docs/openapi/connect-api.json
# and serialized to YAML. Content is unmodified; see openapi/_original/ for the
# byte-for-byte JSON as fetched. Single tag (CONNECT), so no per-tag split applies.
openapi: 3.1.0
info:
  title: Plumma CONNECT API
  description: |
    **Plumma CONNECT** is a phone-intelligence aggregation service. A single call
    takes a phone number in E.164 form plus a list of **commands** (SIM-swap
    check, KYC match, current carrier, porting timestamp, …), routes each command
    to the best available supplier for the number's country, and returns one
    aggregated response.

    ## Authentication
    Every call must carry the `x-plumma-connect-api-key` header. Its value is your
    **X.509 client certificate, base64-encoded**.

    Two different refusals, and they call for two different actions:

    - **`401`** — the certificate itself could not be used: unreadable, expired,
      or not yet valid. A new key is what fixes it.
    - **`403`** — the certificate is fine and the call is not allowed: the header
      was not sent at all, or the certificate was issued for another purpose, or
      the application it names is not one this key is admitted to. A new key
      changes nothing here.

    Both answer `application/problem+json`, and the `detail` field says which of
    the cases it is in a sentence meant to be read.

    ## Live or sandbox: `x-plumma-connect-app-id` decides
    The second header is what tells the two apart, and it is not optional in
    practice — a live call without it does not fail, it silently becomes a
    sandbox call.

    - **Sent** — the call runs **live**: real suppliers, real data, billed per
      served command. The pair (your certificate, that application) must be
      **admitted**; if it is not, the call is refused with `403` and nothing is
      billed. Admission is granted by Plumma per application, and an application
      you have just created is not admitted until it is.
    - **Not sent** — the call runs in the **sandbox**: canned identities, nothing
      billed, and it may only ask about a **demo number**. Any other number is
      refused with `401`.

    The application id is the one shown for that application in the Plumma
    console. There is no way to ask for the live engine any other way: the
    request cannot name its own role.

    ## Billing (per served command)
    You are charged **per command a supplier actually served**, never per API
    call. A command whose country is not covered, whose supplier call failed, or
    that was rejected before routing is **not** billed. A supplier answer that
    means "I hold no data for this" (e.g. a KYC score of `-1`) is a served result
    and **is** billed.

    ## Correlation id
    Never send `X-Correlation-ID` — the server generates it and returns it on the
    response header of every call (success or error). Quote it in support
    requests: it identifies your call server-side.

    ## Model version
    `info.version` matches the model set (PlmRequest / PlmResponse) this contract
    describes. The **same** string is returned in the `version` field of every
    `PlmResponse`, so you can confirm the service matches this document at runtime:
    make any call and compare `response.version` with `info.version` here.

    A standalone JSON Schema for request validation ships alongside this file at
    `plmrequest.schema.json` — point your validator at it to check a payload
    before sending.
  contact:
    name: PLUMMA SRL
    email: info@plumma.it
  license:
    name: Proprietary — © PLUMMA SRL, all rights reserved.
  version: 1.0.2-20260903172027
  x-model-version: 1.0.2-20260903172027
servers:
- url: https://connect.plumma.it/services
  description: Production
tags:
- name: CONNECT
  description: The flagship aggregation endpoint.
paths:
  /api:
    post:
      tags:
      - CONNECT
      summary: Run one or more intelligence commands against a phone number
      operationId: connectApi
      description: |
        Submit a phone number and a non-empty list of commands. The response is a
        single `PlmResponse` in which only the blocks relevant to the served
        commands are populated (unpopulated fields are omitted, not null).

        **Validation happens before routing.** A malformed payload returns `400`
        with the offending field named in `detail`; no supplier is contacted and
        nothing is billed.
      security:
      - ApiKeyAuth: []
      parameters:
      - $ref: '#/components/parameters/ApplicationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlmRequest'
            examples:
              simSwap:
                summary: SIM-swap check (single command)
                value:
                  number: '+393273339145'
                  commands:
                  - sim_swap
              kycMatchStructured:
                summary: KYC match with a structured address
                value:
                  number: '+393273339145'
                  commands:
                  - kyc_match
                  kyc_challenges:
                    name:
                      first_name: Luigi
                      last_name: Armani
                    dob:
                      day: 28
                      month: 2
                      year: 1999
                    address:
                      street: Via Corsini
                      street_no: '21'
                      city: Fanano
                      province: MO
                      postcode: '41021'
                      country: IT
              kycMatchInline:
                summary: KYC match with a free-text (inline) address
                value:
                  number: '+447808226974'
                  commands:
                  - kyc_match
                  kyc_challenges:
                    name:
                      first_name: John
                      last_name: Smith
                    inline_address:
                      address: 23 Omnia Street, London, EC1A 1BB, GB
                      normalize: true
              wideCall:
                summary: Several commands at once
                value:
                  number: '+447808226974'
                  commands:
                  - current_carrier
                  - line_classification
                  - sim_swap
      responses:
        '200':
          description: |
            Routing completed. Inspect `status` / `status_message` for the outcome
            and the populated blocks for the data. HTTP 200 does not by itself mean
            every command was served — read `status`.
          headers:
            X-Correlation-ID:
              $ref: '#/components/headers/CorrelationId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlmResponse'
              examples:
                simSwapServed:
                  summary: SIM-swap served (the operator sent the date of the last swap)
                  value:
                    number: '+393273339145'
                    simswap:
                      last_day: 0
                      risk_indicator: 4
                      simswap_min_threshold: 720
                      date: '2018-10-17T23:00:00.000Z'
                      swapped: false
                      swapped_max_age: 240
                    status: 0
                    status_message: Response from one supplier
                    version: 1.0.2-20260704190444
                simSwapNoSwapFound:
                  summary: SIM-swap served (0 = the operator watched and found no swap)
                  description: The operator holds no swap for this number, and sent no date to place one
                    with. `swapped` / `swapped_max_age` carry the bounded claim - nothing in the last
                    240 hours - while the absent thresholds say there is no swap to bound. Not to be confused
                    with `-1`, which is the operator having nothing to answer with at all.
                  value:
                    number: '+5581986179310'
                    simswap:
                      last_day: 0
                      risk_indicator: 0
                      swapped: false
                      swapped_max_age: 240
                    status: 0
                    status_message: Response from one supplier
                    version: 1.0.2-20260704190444
                simSwapNotApplicable:
                  summary: SIM-swap served (-1 = the operator holds nothing for this identifier)
                  description: The number is well formed and the operator answered `422 SERVICE_NOT_APPLICABLE`
                    - a landline, an M2M or data-only SIM, an MVNO that has not enabled the API. An answer,
                    so the command is served and billed; a call that got no answer would leave the whole
                    `simswap` block absent instead.
                  value:
                    number: '+5519992858171'
                    simswap:
                      risk_indicator: -1
                    status: 0
                    status_message: Response from one supplier
                    version: 1.0.2-20260704190444
                kycMatchServed:
                  summary: KYC match served (0 = no match, -1 = operator holds no data)
                  value:
                    number: '+393273339145'
                    kyc_results:
                      first_name_score: 0
                      last_name_score: 0
                      name_score: 0
                      dob_score: 0
                      country_score: -1
                    status: 0
                    status_message: Response from one supplier
                    version: 1.0.2-20260704190444
        '400':
          description: Malformed or invalid request payload — `detail` names the offending field.
          headers:
            X-Correlation-ID:
              $ref: '#/components/headers/CorrelationId'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetail'
              example:
                type: about:blank
                title: Bad Request
                status: 400
                detail: Plumma kyc_challenges must have either address or inline_address populated
                instance: /services/api
                path: /services/api
                correlation_id: 8b3489e2-dab3-4b1f-a448-2ab46071fac8
        '401':
          description: |
            The certificate could not be used: not a readable X.509 certificate,
            or expired, or not yet valid. `detail` says which. Mint a new key.
          headers:
            X-Correlation-ID:
              $ref: '#/components/headers/CorrelationId'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetail'
        '403':
          description: |
            The certificate is usable and the call is still not allowed: the
            `x-plumma-connect-api-key` header was absent, or the certificate was
            issued for a purpose Connect does not accept, or it is not admitted to
            the application named in `x-plumma-connect-app-id`. `detail` says
            which. A new key does not help — the admission has to change.
          headers:
            X-Correlation-ID:
              $ref: '#/components/headers/CorrelationId'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetail'
        '500':
          description: |
            Unexpected server error. `detail` is generic by design — use the
            `correlation_id` to locate the failure in the logs.
          headers:
            X-Correlation-ID:
              $ref: '#/components/headers/CorrelationId'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetail'
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-plumma-connect-api-key
      description: Your X.509 client certificate, base64-encoded.
  parameters:
    ApplicationId:
      name: x-plumma-connect-app-id
      in: header
      required: false
      description: |
        The application this call belongs to, as shown in the Plumma console.

        **Present** — the call runs against the live suppliers as that
        application, and the pair (certificate, application) has to be admitted:
        it is refused with `403` otherwise, and a value that is not a number is
        refused the same way.

        **Absent** — the call runs in the sandbox and may only ask about a demo
        number. Nothing is billed, and no admission is needed.

        Omitting it is therefore not a smaller version of a live call: it is a
        different call, answered from canned data.
      schema:
        type: string
        pattern: ^[0-9]+$
      example: '11'
  headers:
    CorrelationId:
      description: Server-generated request id, echoed on every response. Never sent by the client.
      schema:
        type: string
        format: uuid
        example: 8b3489e2-dab3-4b1f-a448-2ab46071fac8
  schemas:
    ProblemDetail:
      type: object
      description: RFC 7807 problem document returned on every 4xx/5xx.
      properties:
        type:
          type: string
          format: uri
          example: about:blank
        title:
          type: string
          example: Bad Request
        status:
          type: integer
          format: int32
          example: 400
        detail:
          type: string
          example: Plumma commands list cannot be null or empty
        instance:
          type: string
          example: /services/api
        path:
          type: string
          example: /services/api
        correlation_id:
          type: string
          format: uuid
          example: 8b3489e2-dab3-4b1f-a448-2ab46071fac8
    Address:
      type: object
      properties:
        street:
          type: string
          minLength: 1
          maxLength: 100
          examples:
          - Via Corsini
        street_no:
          type: string
          maxLength: 10
          pattern: '[A-Za-z0-9\-/]+'
          examples:
          - '21'
        city:
          type: string
          minLength: 1
          maxLength: 50
          examples:
          - Fanano
          $comment: Server enforces [\p{L}'\-\s]+ ; omitted here for cross-engine portability.
        province:
          type: string
          maxLength: 50
          examples:
          - MO
        postcode:
          type: string
          pattern: '[0-9A-Za-z]{4,10}'
          examples:
          - '41021'
        national_id:
          type: string
          maxLength: 50
          pattern: '[A-Za-z0-9\-]+'
        country:
          type: string
          pattern: '[A-Za-z]{2,3}'
          description: ISO country code (2-3 letters).
          examples:
          - IT
        normalize:
          type: boolean
          default: false
        house_number_extension:
          type: string
      additionalProperties: false
      description: Structured postal address. Mutually exclusive with inline_address.
      required:
      - street
      - street_no
      - city
      - postcode
      - country
    Dob:
      type: object
      properties:
        day:
          type: integer
          minimum: 1
          maximum: 31
          examples:
          - 28
        month:
          type: integer
          minimum: 1
          maximum: 12
          examples:
          - 2
        year:
          type: integer
          minimum: 1900
          maximum: 2100
          examples:
          - 1999
      additionalProperties: false
      description: Date of birth. Either fully specified (day+month+year) or entirely omitted — a partial
        DOB is rejected.
      dependencies:
        day:
        - month
        - year
        month:
        - day
        - year
        year:
        - day
        - month
    InlineAddress:
      type: object
      properties:
        address:
          type: string
          minLength: 1
          examples:
          - 23 Omnia Street, London, EC1A 1BB, GB
        normalize:
          type: boolean
          default: false
      additionalProperties: false
      description: Free-text address as a single string. Mutually exclusive with address.
      required:
      - address
    KycChallenges:
      type: object
      properties:
        dob:
          $ref: '#/components/schemas/Dob'
        name:
          $ref: '#/components/schemas/Name'
        address:
          $ref: '#/components/schemas/Address'
        email:
          type: string
          format: email
          examples:
          - john.smith@example.com
        inline_address:
          $ref: '#/components/schemas/InlineAddress'
        return_address:
          type: boolean
          description: Ask the supplier to return the normalized address block.
        gender:
          type: string
      additionalProperties: false
      description: 'Data to match for `kyc_match`. Must carry EXACTLY ONE address form: `address` (structured)
        or `inline_address` (free text) — never both, never neither.'
      oneOf:
      - $comment: structured address present, inline absent
        required:
        - address
        not:
          required:
          - inline_address
      - $comment: inline address present, structured absent
        required:
        - inline_address
        not:
          required:
          - address
    Name:
      type: object
      properties:
        first_name:
          type: string
          minLength: 2
          maxLength: 100
          $comment: Server also requires ^[\p{L}][\p{L}'\-\s]*$; \p{L} is not portable across JSON Schema
            regex engines, so only length is validated here and the exact character rule stays server-side.
        last_name:
          type: string
          minLength: 2
          maxLength: 100
        middle_name:
          type: string
          minLength: 2
          maxLength: 100
        name_kana_hankaku:
          type: string
        name_kana_zenkaku:
          type: string
        family_name_at_birth:
          type: string
          minLength: 2
          maxLength: 100
      additionalProperties: false
    allowedCommandValues:
      type: string
      enum:
      - line_classification
      - current_carrier
      - issuing_carrier
      - porting_timestamp
      - porting_logs
      - network_presence
      - roaming_intel
      - deactivation_point
      - churn_tracker
      - sim_swap
      - port_fraud_shield
      - digital_footprint
      - age_verification
      - kyc_match
      - divert_detector
      - commercial_segment
      - tenure_period
    PlmRequest:
      type: object
      properties:
        number:
          type: string
          minLength: 1
          pattern: ^\+?[1-9][0-9]{6,14}$
          description: Phone number in international form. Accepted with or without a leading '+'; the
            server normalises to E.164 and rejects numbers libphonenumber considers invalid (a regex cannot
            fully replicate that check).
          examples:
          - '+393273339145'
          - '447808226974'
        commands:
          type: array
          items:
            $ref: '#/components/schemas/allowedCommandValues'
          minItems: 1
          description: Non-empty list of commands to run. Duplicate/unknown values are rejected (400).
        kyc_challenges:
          $ref: '#/components/schemas/KycChallenges'
        tenure_period:
          type: integer
          description: Tenure window in days. Required only when the `tenure_period` command is requested.
          examples:
          - 90
      additionalProperties: false
      required:
      - number
      - commands
      allOf:
      - $comment: When kyc_match is requested, kyc_challenges is required.
        if:
          properties:
            commands:
              contains:
                const: kyc_match
          required:
          - commands
        then:
          required:
          - kyc_challenges
      - $comment: When the tenure_period command is requested, the tenure_period value is required.
        if:
          properties:
            commands:
              contains:
                const: tenure_period
          required:
          - commands
        then:
          required:
          - tenure_period
    AgeVerification:
      type: object
      properties:
        verified:
          type: integer
        threshold:
          type: integer
    ChurnTracker:
      type: object
      properties:
        operator:
          type: string
        action:
          type: string
        ts:
          type: string
          format: date-time
          description: When the deactivation/churn event happened, ISO-8601 UTC.
          examples:
          - '2025-11-02T08:30:00.000Z'
        number:
          type: integer
        number2:
          type: integer
    CurrentCarrier:
      type: object
      properties:
        lrn: {}
        mcc:
          type: string
        mnc:
          type: string
        name:
          type: string
        spid:
          type: string
        ocn: {}
    DigitalFootprint:
      type: object
      properties:
        whatsapp:
          type: integer
        telegram:
          type: integer
        amazon:
          type: integer
        google:
          type: integer
        office365:
          type: integer
        instagram:
          type: integer
        linkedin:
          type: integer
        twitter:
          type: integer
        skype:
          type: integer
        flipkart:
          type: integer
        viber:
          type: integer
        bukalapak:
          type: integer
        facebook:
          type: integer
        error:
          type: integer
        error_description:
          type: string
    DivertDetector:
      type: object
      properties:
        unconditional_call_forward:
          type: integer
    IssuingCarrier:
      type: object
      properties:
        mcc:
          type: string
        mnc:
          type: string
        name:
          type: string
        spid:
          type: string
        ocn: {}
    KycResults:
      type: object
      properties:
        first_name_score:
          type: integer
        last_name_score:
          type: integer
        middle_name_score:
          type: integer
        name_score:
          type: integer
        address_score:
          type: integer
        street_no_score:
          type: integer
        street_score:
          type: integer
        city_score:
          type: integer
        postcode_score:
          type: integer
        dob_score:
          type: integer
        province_score:
          type: integer
        country_score:
          type: integer
        national_id_score:
          type: integer
        email_score:
          type: integer
    NormalizedAddress:
      type: object
      properties:
        street_no:
          type: string
        street:
          type: string
        city:
          type: string
        province:
          type: string
        postcode:
          type: string
        country:
          type: string
    PortingLogs:
      type: object
      properties:
        plmnetwork:
          type: integer
        i_type:
          type: string
        action:
          type: string
        ts:
          type: string
          format: date-time
          description: When the porting event happened, ISO-8601 UTC.
          examples:
          - '2016-03-11T09:15:00.000Z'
    QdrHistory:
      type: object
      properties:
        service:
          type: string
        source:
          type: string
        ts:
          type: string
          format: date-time
          description: When the query-detail record was observed, ISO-8601 UTC.
          examples:
          - '2026-08-01T07:00:00.000Z'
        plmnetwork:
          type: string
    RoamingNetwork:
      type: object
      properties:
        lrn: {}
        mcc:
          type: string
        mnc:
          type: string
        name:
          type: string
        spid:
          type: string
        ocn: {}
    Simswap:
      type: object
      properties:
        last_day:
          type: integer
          description: '1 when the last SIM change falls within the last 24 hours, 0 otherwise. Absent
            when neither can be stated: when the operator answered only that a swap happened somewhere
            inside a window wider than 24 h, and when it refused or held nothing.'
        risk_indicator:
          type: integer
          description: '0 = the operator watched and found no SIM swap, the lowest risk there is. 1 =
            swap in the last 24 h, 2 = last 72 h, 3 = last 30 days, 4 = older than 30 days, which is stated
            only when the operator''s own monitored period is at least that long. Two negatives are answers,
            not failures: -1 = the operator answered that it holds nothing servable for this number, -2
            = the operator answered refusing on this number. A command whose call got no answer leaves
            the whole block absent instead - which is the difference between an operator with nothing
            to say and a supplier that never replied.'
          examples:
          - 0
        simswap_min_threshold:
          type: integer
          description: Lower bound, in hours, of the window the last SIM change falls into. Absent when
            there is no lower bound (the change is more recent than 24 h) and when there is no swap to
            bound at all (`risk_indicator` 0).
        simswap_max_threshold:
          type: integer
          description: Upper bound, in hours, of the window the last SIM change falls into. Absent when
            there is no upper bound (the change is older than the monitored period) and when there is
            no swap to bound at all (`risk_indicator` 0). When the operator answered with the boolean
            alone, this is the window that boolean referred to.
        date:
          type: string
          format: date-time
          description: When the last SIM change happened, ISO-8601 UTC. Absent when the supplier answered
            but holds no date.
          examples:
          - '2018-10-17T23:00:00.000Z'
        swapped:
          type: boolean
          description: Whether a SIM swap happened within the last `swapped_max_age` hours. Meaningless
            without that window, which is why the two always travel together. Absent when neither the
            date nor the supplier's own answer allowed it to be determined.
          examples:
          - false
        swapped_max_age:
          type: integer
          description: The window, in hours, that `swapped` refers to.
          examples:
          - 240
      description: SIM-swap block. `date` and the `risk_indicator` / threshold trio are the supplier's
        own view of the last SIM change; `swapped` / `swapped_max_age` are the CAMARA pair and answer
        one fixed question, so two suppliers can be compared.
    PlmResponse:
      type: object
      properties:
        number:
          type: string
        kyc_results:
          $ref: '#/components/schemas/KycResults'
        normalize_address:
          type: string
        current_carrier:
          $ref: '#/components/schemas/CurrentCarrier'
        ported:
          type: boolean
        issuing_carrier:
          $ref: '#/components/schemas/IssuingCarrier'
        ported_date:
          type: string
          format: date-time
          description: When the number was ported to its current carrier, ISO-8601 UTC. Suppliers that
            only hold the calendar day are rendered at midnight UTC; `ported_date_type` declares how precise
            the underlying date is.
          examples:
          - '2016-03-11T00:00:00.000Z'
        ported_date_type:
          type: string
          description: How precise `ported_date` is, as declared by the supplier that answered.
          examples:
          - FULL
        present:
          type: string
        type:
          type: string
        etype:
          type: string
        porting_logs:
          type: array
          items:
            $ref: '#/components/schemas/PortingLogs'
          description: Porting events known for the number. Absent when the command was not requested
            or no supplier held any row — never an empty array.
        is_roaming:
          type: string
        roaming_network:
          $ref: '#/components/schemas/RoamingNetwork'
        portfraud:
          type: string
        qdr_history:
          type: array
          items:
            $ref: '#/components/schemas/QdrHistory'
        status:
          type: integer
        status_message:
          type: string
        simswap:
          $ref: '#/components/schemas/Simswap'
        plm_score:
          type: integer
        age_verification:
          $ref: '#/components/schemas/AgeVerification'
        digital_footprint:
          $ref: '#/components/schemas/DigitalFootprint'
        divert_detector:
          $ref: '#/components/schemas/DivertDetector'
        churn_tracker:
          type: array
          items:
            $ref: '#/components/schemas/ChurnTracker'
          description: Deactivation/churn events known for the number, most useful as a recency signal.
            Absent when the command was not requested or no supplier held any row — never an empty array.
        normalized_address:
          $ref: '#/components/schemas/NormalizedAddress'
        market_segment:
          type: string
        version:
          type: string
          readOnly: true
          description: The model-set version that produced this response. Compare it with `info.version`
            of this document to confirm the service matches the contract you are reading.