aria-partner-api

Members

The member is created with the account (Create account). This API does not support adding another member to an account. There is no POST /api/v1/members/.

Resource Path Purpose
Account members /api/v1/accounts/{account_id}/members/ GET members on the account (Account members)
Members (collection) /api/v1/members/ GET paginated profile summaries (List members)
Member /api/v1/members/{member_id}/ GET full profile (Retrieve one member); PATCH birth fields (Update one member); DELETE one profile (Delete a member)
Profile attributes /api/v1/members/{member_id}/names/ and the other nested routes Add or remove names, addresses, emails, phone numbers, usernames, and employers (Add or remove profile attributes)
Member products /api/v1/members/{member_id}/products/ GET every product on the member (Member products)
Member product /api/v1/members/{member_id}/product/ POST add an on-demand scan product (Add a product)
Subscription /api/v1/members/{member_id}/subscriptions/{product_id}/ GET one subscription and its change history (Subscriptions)
Change subscription /api/v1/members/{member_id}/change_subscription/{new_product_id}/ POST move the member to another subscription product (Change subscription)
Scan credits /api/v1/members/{member_id}/credits/ GET credit balances (Scan credits)
Scan credit /api/v1/members/{member_id}/credits/{product_id}/ GET one product’s credit balance (One product)
Add credits /api/v1/members/{member_id}/credits/{product_id}/add/ POST assign credits from the org pool (Add credits)
Member scans /api/v1/members/{member_id}/scans/ GET scan schedule, in-progress jobs, and completed scan history (Scans)
Scan one member /api/v1/members/{member_id}/scan/{product_id}/ POST spend one credit of that product and queue a scan (Scan one member)
Scan many members /api/v1/scan/ POST queue scans for many member ids or account ids (Scan many members)
Member exposures /api/v1/members/{member_id}/exposures/ GET paginated exposures for one profile (Exposure API)

Member summary

Each object in an account’s members, each row from GET /api/v1/accounts/{account_id}/members/, and each row in GET /api/v1/members/ → results uses this summary.

Field Type Description
member_id integer Profile (member) id
primary_name string Display name
primary_email string | null Primary email when set
is_primary boolean true for the primary member on the account. The member created with the account is true
profile_threat_score number | null Member-level threat score (Threat score API). Available only to select beta users. null otherwise
account_id integer Present on GET /api/v1/members/ only — the account this profile belongs to

Profile fields

These fields are sent on POST /api/v1/accounts/ beside email and product_ids. email on that call is the account login. The fields below are the member profile. They are optional. Without a name, city, state, or birth year, scan and removal are severely limited.

Nested attribute POST bodies use the same shapes. The member is returned on account create (201).

Minimal profile

{
  "birth_year": 1980,
  "names": [
    { "first_name": "Bob", "last_name": "Example", "no_middle_name": true }
  ],
  "addresses": [
    { "city": "New York City", "state": "NY", "country": "US" }
  ],
  "emails": [
    { "email": "bob@example.com" }
  ]
}

Emails and phone numbers

By sending an email or phone number, you agree you have verified that the end user owns it. In v1, Kanary does not provide phone or email verification endpoints.

Submit addresses that are already valid and acceptable for scanning.

emails — each item is { "email": "<address>" }:

Rule Detail
Validator Must be a valid email address
Format Must be a valid email address shape (e.g. user@example.com); not a display name, not a bare domain
Normalization Stored lowercase in Kanary (local@Example.com → local@example.com)
Duplicates Rejected if the same address already exists on that member

phone_numbers — each item is { "phone_number": "<e164>" }:

Rule Detail
Validator Must be a valid international phone number
Format International / E.164: leading +, country code, then national number (e.g. +17015551234, +14155552671)
Validity Must be a valid number for its region; national-only strings without country code (e.g. 7015551234) typically fail
Duplicates Rejected if the same number already exists on that member
"emails": [{ "email": "bob@example.com" }],
"phone_numbers": [{ "phone_number": "+17015551234" }]

Full profile

{
  "birth_year": 1980,
  "birth_month": 6,
  "birth_day": 15,
  "names": [
    {
      "first_name": "Bob",
      "last_name": "Example",
      "middle_name": "Q",
      "suffix": "Jr",
      "no_middle_name": false,
      "is_primary": true
    }
  ],
  "addresses": [
    {
      "street_address": "123 Main St",
      "unit": "4B",
      "city": "New York City",
      "state": "NY",
      "postal_code": "82230",
      "country": "US",
      "is_primary": true
    }
  ],
  "phone_numbers": [{ "phone_number": "+17015551234" }],
  "emails": [{ "email": "bob@example.com" }],
  "usernames": [{ "username": "bob_example" }],
  "employers": [{ "employer": "Acme Corp", "industry": "Tech and Engineering" }]
}

Errors: invalid or unaccepted inputs

400 Bad Request with field errors, for example:

Input Typical error
Invalid ISO country on address country: must be valid ISO3166 alpha-2
US/GB address without state state: required for US/GB
Invalid US/GB state code state: must be valid state/region code
Name < 2 characters first_name / last_name validation
Middle name missing when no_middle_name is false middle_name: required
Both middle_name and no_middle_name: true conflicting middle name flags
Invalid email format emails / email: Enter a valid email address
Duplicate email on member Email already exists for member
Invalid or non-international phone phone_numbers / phone_number: phonenumber validation error

Account members

GET /api/v1/accounts/{account_id}/members/

Active members on this account. Each row is a member summary. account_id is on the envelope. See Pagination.

Query parameters

Param Default Max Description
limit 100 250 Members per page
offset 0 — Starting index into the list

Example

GET /api/v1/accounts/12345/members/

Success (200 OK)

{
  "account_id": 12345,
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "member_id": 67890,
      "primary_name": "Bob Example",
      "primary_email": "bob@example.com",
      "is_primary": true,
      "profile_threat_score": 6.2
    }
  ]
}

Errors

Status Condition
400 Invalid limit or offset
401 Missing or invalid API key
403 Partner inactive
404 Account not found

List members

GET /api/v1/members/

Returns a paginated list of member summaries. See Pagination.

Query parameters

Param Default Max Description
limit 100 250 Members per page (Pagination)
offset 0 — Starting index into the member list

Example

GET /api/v1/members/?limit=100&offset=0

Success (200 OK)

{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "account_id": 22392,
      "member_id": 67890,
      "primary_name": "Bob Example",
      "primary_email": "bob@example.com",
      "is_primary": true,
      "profile_threat_score": 6.2
    }
  ]
}

Errors

Status Condition
400 Invalid limit or offset
401 Missing or invalid API key
403 Partner inactive

Member products

GET /api/v1/members/{member_id}/products/

Every product on the member. Active rows first, then terminated subscriptions and on-demand rows with credits_remaining 0. Same object as Product API. See Pagination.

Query parameters

Param Default Max Description
limit 100 250 Products per page
offset 0 — Starting index into the sorted list

Example

GET /api/v1/members/70001/products/

Success (200 OK)

{
  "member_id": 70001,
  "count": 2,
  "next": null,
  "previous": null,
  "results": [
    {
      "product_id": 201,
      "product_name": "Core Scan",
      "description": "On-demand scan of the Core catalog.",
      "charge_type": "on_demand",
      "billing_frequency": "one_time",
      "price_per_seat": 299,
      "credits_remaining": 1
    },
    {
      "product_id": 301,
      "product_name": "Trial Scan",
      "description": "On-demand scan of the trial catalog.",
      "charge_type": "on_demand",
      "billing_frequency": "one_time",
      "price_per_seat": 499,
      "credits_remaining": 0
    }
  ]
}

Errors

Status Condition
400 Invalid limit or offset
401 Missing or invalid API key
403 Partner inactive
404 Member not found

Add a product

POST /api/v1/members/{member_id}/product/

A subscription product_id on this call returns 400 (error: use_subscription_update). Change an existing subscription with Change subscription.

quantity is credits drawn from the org pool (GET /api/v1/credits/) for that on-demand product. This call does not charge. Subscription seats are purchased with POST /api/v1/seats/. On-demand credits are purchased with POST /api/v1/credits/.

POST /api/v1/members/70001/product/
{
  "product_id": 201,
  "quantity": 1,
  "idempotency_key": "product-70001-201-a1"
}
Field Required Description
product_id Yes An on-demand product from GET /api/v1/products/
quantity No Credits to assign from the org pool. Default 1
idempotency_key No Up to 64 characters. Repeating a request with the same key returns the original assignment and does not draw from the pool again

Success (200 OK)

Returns the account object plus member_id and exposures_activated. The product is assigned on that member. When is_primary is true, the account stores it in products, and in primary_subscription_product_id when the product is a subscription. exposures_activated is how many exposures moved from BLOCKED to ACTIVE when the member only had on-demand products and a subscription is added. The count is 0 when the added product is on-demand.

{
  "account_id": 12000,
  "member_id": 70001,
  "status": "ACTIVE",
  "primary_subscription_product_id": null,
  "products": [
    {
      "product_id": 201,
      "product_name": "Core Scan",
      "description": "On-demand scan of the Core catalog.",
      "charge_type": "on_demand",
      "billing_frequency": "one_time",
      "price_per_seat": 299,
      "credits_remaining": 1
    }
  ],
  "exposures_activated": 0,
  "members": [
    {
      "member_id": 70001,
      "primary_name": "Scan User",
      "primary_email": "scan@company.com",
      "is_primary": true,
      "profile_threat_score": null
    }
  ]
}

Errors

Status Condition
400 product_id missing or unknown; product_id is a subscription (error: use_subscription_update); quantity not a positive integer; idempotency_key reused with a different body (error: idempotency_conflict); account is TERMINATED
401 Missing or invalid API key
402 Org pool available for that product is less than quantity, including when it is 0 (error: insufficient_credits; includes product_id and available). Buy with POST /api/v1/credits/ and retry
403 Partner inactive
404 Member not found

Subscriptions

A subscription is {member_id} plus {product_id}. There is no separate subscription id. primary_subscription_product_id on the account is that product_id when the member is primary, or null when the member has no subscription.

Retrieve a subscription

GET /api/v1/members/{member_id}/subscriptions/{product_id}/

GET /api/v1/members/67890/subscriptions/103/
{
  "member_id": 67890,
  "product_id": 103,
  "product_name": "Advanced",
  "description": "Scan and removal subscription on the Advanced catalog.",
  "charge_type": "subscription",
  "billing_frequency": "monthly",
  "price_per_seat": 1999,
  "credits_remaining": null,
  "changes": [
    {
      "id": 12,
      "action": "upgrade",
      "from_product_id": 101,
      "to_product_id": 103,
      "exposures_blocked": 0,
      "created_at": "2026-10-05T15:00:00.000000+00:00"
    }
  ]
}

Change subscription

POST /api/v1/members/{member_id}/change_subscription/{new_product_id}/

{new_product_id} is the subscription to move to, from GET /api/v1/products/.

The same seat moves to that product. No seat is added and none is returned. primary_subscription_product_id becomes {new_product_id}. An upgrade from Core to Advanced is charged on the partner payment method at the time of the change (Upgrade Core to Advanced).

Each change is kept on the subscription in changes, newest first. action is upgrade or downgrade when the product type changes, and billing_frequency when only the term changes.

Moving to a product that covers less, such as Advanced to Core, marks exposures BLOCKED when the new product does not cover them. Exposures already IGNORED stay IGNORED. The count is exposures_blocked.

POST /api/v1/members/67890/change_subscription/103/
{
  "idempotency_key": "upgrade-67890-103-a1"
}
Field Required Description
idempotency_key No Up to 64 characters. Repeating a request with the same key returns the original change and does not charge again

Success (200 OK)

Returns the subscription, including the new row in changes. exposures_blocked is 0 when the new product still covers the member’s exposures.

Errors

Status Condition
400 {new_product_id} is not a subscription, is the product already on the member, or the member has no subscription (error: invalid_subscription_change); idempotency_key reused with a different body (error: idempotency_conflict); account is TERMINATED
401 Missing or invalid API key
402 No partner payment method on file (error: payment_method_required), or the charge was declined (error: payment_failed)
403 Partner inactive
404 Member or product not found

Retrieve one member

GET /api/v1/members/{member_id}/

Resolve {member_id} from Account members, List members, results[].members[].member_id on GET /api/v1/accounts/, or the account-create response.

Errors

Status Condition
401 Missing or invalid API key
403 Partner inactive
404 Member not found

Member response fields

Returned on GET /api/v1/members/{member_id}/, and PATCH /api/v1/members/{member_id}/ (200). Read-only; not accepted on create or update requests.

Field Description
is_primary true for the primary member on the account. The member created with the account is true
profile_threat_score Normalized threat score for this profile (numeric). Available only to select beta users.

Update one member

PATCH /api/v1/members/{member_id}/

Accepts only scalar birth fields (birth_year, birth_month, birth_day). Sending any other field (e.g. names, addresses) returns 400 Bad Request. Use the nested attribute endpoints to update other profile data.

Success: 200 OK with the updated member body (same shape as retrieve).

Errors

Status Condition
400 Invalid birth fields, or unrecognised fields sent in the request body
401 Missing or invalid API key
403 Partner inactive
404 Member not found

Delete a member

DELETE /api/v1/members/{member_id}/

Requires ?confirm=true. Clears all PII immediately (not recoverable), returns a confirmation message, and preserves metadata only. Does not delete sibling profiles on the account. Deleting the whole account clears PII across every profile (Delete an account). You must have at least one primary member associated with an account for it to be active, you will not be able to delete the primary member and instead should delete the account object.

Example

DELETE /api/v1/members/67890/?confirm=true

Send confirm=true only after the end user has explicitly agreed in your UI — never hard-code it.

Success (200 OK)

{
  "message": "Member deleted. All personally identifiable information has been permanently cleared and cannot be recovered.",
  "member_id": 67890,
  "terminated_at": "2026-05-19T14:30:00Z",
  "metadata": {
    "account": {
      "account_id": 12345,
      "created_at": "2026-01-15T10:00:00Z",
      "terminated_at": null
    },
    "member": {
      "created_at": "2026-01-20T09:00:00Z",
      "terminated_at": "2026-05-19T14:30:00Z"
    },
    "results_found_count": 42,
    "profile_attributes_count": 5
  }
}

Errors

Status Condition
400 confirm query parameter missing or not true
404 Member not found

Add or remove profile attributes

Nested routes (under /api/v1/members/{member_id}/):

Resource List Add Update Remove
Names GET .../names/ POST .../names/ (none) DELETE .../names/{name_id}/
Emails GET .../emails/ POST .../emails/ (none) DELETE .../emails/{email_id}/
Addresses GET .../addresses/ POST .../addresses/ (none) DELETE .../addresses/{address_id}/
Phone numbers GET .../phone_numbers/ POST .../phone_numbers/ (none) DELETE .../phone_numbers/{id}/
Usernames GET .../usernames/ POST .../usernames/ (none) DELETE .../usernames/{id}/
Employers GET .../employers/ POST .../employers/ (none) DELETE .../employers/{id}/
Scans GET .../scans/ (none) (none) (none)
Exposures GET .../exposures/ (none) (none) (none)

POST bodies use the same field shapes as in Profile fields (e.g. first_name / last_name for names). See Scans for GET .../scans/ and Exposure API for GET .../exposures/.

Success: 201 Created on POST. Attribute DELETE behavior is described below.

Deleting a name, address, or other attribute

DELETE on nested routes (.../names/{id}/, .../addresses/{id}/, emails, phone numbers, usernames, employers) is not a silent field removal. In-progress exposures depend on the PII they were found with.

When an attribute is deleted, any in-progress exposure tied to that profile and the old information is deleted. Kanary does not keep that PII, so those exposures are no longer worked.

Members cannot have an account without the minimum data required. If you try to delete PII that would leave an account in an empty state, you will see an error.

Example

DELETE /api/v1/members/67890/names/111/?confirm=true

Omit confirm or any value other than true returns 400 Bad Request with an explanation of the delete behavior and required acknowledgements.

Success (200 OK)

{
  "message": "Attribute removed. Active exposures tied to the previous information have been deleted. Add updated PII and run a new scan to find and mitigate exposures under the new profile data.",
  "attribute_type": "name",
  "attribute_id": 111,
  "exposures_deleted_count": 3
}

Set primary flag (names, emails, addresses)

Only 1 attribute can be primary in each category; setting a new attribute to is_primary: true flips the old one to false.

Set is_primary on POST when adding a record, or call the set_primary action on an existing one:

POST /api/v1/members/67890/addresses/
{
  "street_address": "123 Main St",
  "city": "New York City",
  "state": "NY",
  "country": "US",
  "is_primary": true
}
POST /api/v1/members/67890/names/111/set_primary
POST /api/v1/members/67890/addresses/333/set_primary
POST /api/v1/members/67890/emails/222/set_primary

Setting is_primary: true on one name, email, or address makes that record the profile’s preferred name, primary email, or most recent/active address.

Minimum data protection on DELETE

DELETE is rejected with 400 if it would leave the profile without at least one of each required attribute. The email minimum applies only to primary members (accounts require a primary member to have a contact email; secondary profiles may delete their last email).

Scans

GET /api/v1/members/{member_id}/scans/

A paginated list of scans: in-progress (PENDING, QUEUED, RUNNING) and completed (SUCCESSFUL, FAILED). See Pagination.

Subscription products scan on a built-in cadence (Product API). Queue an on-demand scan with Scan API.

Query parameters

Param Default Max Description
limit 100 250 Scan jobs per page (Pagination)
offset 0 — Starting index into the scan history

Example

GET /api/v1/members/67890/scans/?limit=50&offset=0

Success (200 OK)

{
  "member_id": 67890,
  "next_scan_date": "2026-05-22T08:00:00Z",
  "count": 3,
  "next": null,
  "previous": null,
  "results": [
    {
      "id": "job-uuid-3",
      "site_id": 12,
      "site_domain": "site.domain",
      "status": "in_progress",
      "created_at": "2026-05-21T09:00:00Z",
      "completed_at": null
    },
    {
      "id": "job-uuid-1",
      "site_id": 12,
      "site_domain": "site.domain",
      "status": "success",
      "created_at": "2026-05-18T10:00:00Z",
      "completed_at": "2026-05-18T10:42:00Z"
    },
    {
      "id": "job-uuid-2",
      "site_id": 45,
      "site_domain": "site.domain",
      "status": "failure",
      "created_at": "2026-05-15T14:00:00Z",
      "completed_at": "2026-05-15T14:05:00Z"
    }
  ]
}

Response fields

Field Type Description
member_id integer Profile queried
next_scan_date string (ISO 8601, UTC) | null perform_at of the earliest PENDING (not yet claimed) job for this member. null if no PENDING jobs exist. Jobs already QUEUED or RUNNING are excluded — they are in flight, not scheduled.
count integer Total scan jobs for this member
next string | null URL for the next page, or null
previous string | null URL for the previous page, or null
results array Current page of scan jobs (in-progress and completed)

Each entry in results:

Field Type Description
id string (UUID) Scan job id
site_id integer Site the job ran against (Site API)
site_domain string Domain of the site (e.g. site.domain)
status string in_progress, success, or failure
created_at string (ISO 8601, UTC) When the scan job was created
completed_at string (ISO 8601, UTC) | null When the job finished; null while status is in_progress

results is ordered newest created_at first. Follow next until it is null to retrieve full history.

Errors

Status Condition
400 Invalid limit or offset
401 Missing or invalid API key
403 Partner inactive
404 Member not found