baref00t.io
v1.0

API Reference

Integrate baref00t.io into your stack. Manage customers, trigger assessments, and automate security workflows through the Partner and Distributor APIs.

Overview

baref00t.io exposes two REST APIs that let you automate security assessment workflows. The Partner API is for MSPs who manage their own customers and run assessments against those tenants. The Distributor API is for organisations that provision and manage multiple partner accounts underneath a single billing relationship.

Both APIs accept and return JSON. All requests must be made over HTTPS. HTTP requests are rejected.

Authentication

Every request must include an API key in a custom header. Partner keys and Distributor keys use separate headers and separate prefixes so the server can route your request to the correct API surface.

Partner API Key

Pass your key in the X-Partner-Key header. Partner keys always start with pk_live_.

curl https://api.baref00t.io/api/v1/partner/me \
  -H "X-Partner-Key: pk_live_abc123def456"

Distributor API Key

Pass your key in the X-Distributor-Key header. Distributor keys always start with dk_live_.

curl https://api.baref00t.io/api/v1/distributor/me \
  -H "X-Distributor-Key: dk_live_xyz789ghi012"

Key Management

Each partner account can hold a maximum of two active API keys at any time. Use the Generate API Key and Revoke API Key endpoints to rotate keys with zero downtime: generate a new key in slot 2 while slot 1 is still active, migrate your systems, then revoke slot 1.

Never expose your API key in client-side code, public repositories, or browser requests. Keys carry the same privileges as your partner or distributor account.

Base URLs

APIBase URL
Partnerhttps://api.baref00t.io/api/v1/partner/
Distributorhttps://api.baref00t.io/api/v1/distributor/

All endpoint paths in this reference are shown relative to the base URL. For example, GET /v1/partner/me means you send the request to https://api.baref00t.io/api/v1/partner/me.

Rate Limits

APILimitWindow
Partner60 requestsper minute, per key
Distributor120 requestsper minute, per key

When you exceed the limit, the API returns a 429 status with the error code RATE_LIMITED. The response includes a Retry-After header indicating how many seconds to wait before retrying.

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Retry after 12 seconds."
  }
}

Error Handling

All error responses follow a consistent envelope. The HTTP status code indicates the error category, and the response body contains a machine-readable code and a human-readable message.

{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human readable description of what went wrong."
  }
}

Error Codes

CodeStatusDescription
MISSING_KEY401No API key header was provided in the request.
INVALID_KEY401The API key is malformed, expired, or revoked.
RATE_LIMITED429You have exceeded the rate limit for your API tier.
PARTNER_INACTIVE403The partner account is suspended or pending activation.
DISTRIBUTOR_INACTIVE403The distributor account is suspended or pending activation.
INVALID_JSON400The request body is not valid JSON.
MISSING_FIELD400A required field is missing from the request body.
INVALID_PRODUCT400The product identifier is not recognised.
PRODUCT_NOT_ALLOWED403Your plan does not include the requested product.
RUN_LIMIT_REACHED403You have reached the monthly assessment run limit for your plan.
NOT_FOUND404The requested resource does not exist or you do not have access.
MAX_KEYS409You already have the maximum number of API keys (2).
TOO_MANY400The bulk request exceeds the maximum batch size.
FEATURE_NOT_ENABLED403The partner record lacks an admin-granted add-on (e.g. prospecting) required for this endpoint.
MAIL_DISABLED400Partner mailProvider is off. Pass skipEmail: true to mint URLs without sending, or change the mail mode to resend / microsoft.
EMAIL_FAILED502The email provider rejected all recipient sends. The response includes a recipients breakdown plus assessment/customer/run ids so the run row is recoverable.

Webhooks

Configure a webhook URL in your partner or distributor portal to receive real-time event notifications. Webhooks are delivered as POST requests with a JSON body to the URL you specify.

Events

EventWhen it fires
assessment.startedAn assessment run has been queued and is beginning execution.
assessment.completedAn assessment run has finished and results are available.
assessment.failedAn assessment run encountered an error and could not complete.
customer.createdA new customer record was created.
customer.updatedA customer record was modified.
customer.deletedA customer record was removed.
partner.activatedA sub-partner account has been activated (Distributor only).
partner.suspendedA sub-partner account has been suspended (Distributor only).
key.createdA new API key was generated for the account.
key.revokedAn API key was revoked.
lead.createdA new pipeline lead was created.
lead.enrichedApollo + Hunter + M365 OIDC enrichment completed for a lead.
lead.outreach_sentAt least one consent email succeeded after send-consent (or skipEmail: true was used).
lead.stage_changedServer-side or partner-set stage transition (Won/Lost are partner-driven).
lead.consentedThe customer clicked the consent URL and granted Microsoft admin consent.
lead.report_deliveredThe assessment report was delivered to the customer.

Payload Format

{
  "id": "evt_a1b2c3d4e5f6",
  "type": "assessment.completed",
  "timestamp": "2026-04-03T08:15:22Z",
  "data": {
    "assessmentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "customerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "product": "essential-eight",
    "status": "completed",
    "score": 72
  }
}

Signature Verification

Every webhook request includes an X-Baref00t-Signature header containing an HMAC-SHA256 signature of the raw request body. Verify this signature using the webhook secret displayed in your portal to ensure the request originated from baref00t.io.

// Pseudocode: verify webhook signature
expected = HMAC-SHA256(webhook_secret, raw_request_body)
received = request.headers["X-Baref00t-Signature"]

// Use a constant-time comparison to prevent timing attacks
valid = constant_time_equal(expected, received)

Delivery

Webhooks are delivered with a timeout of 10 seconds. If your endpoint returns a non-2xx status code or times out, the delivery is retried up to 5 times with exponential backoff (1 min, 5 min, 30 min, 2 hr, 12 hr). After all retries are exhausted, the event is marked as failed in your portal's webhook log.

Your webhook endpoint should return a 200 status code as quickly as possible. Process the event payload asynchronously to avoid timeouts.

Partner API

The Partner API lets MSPs manage customers and run security assessments through a single API key. All endpoints require the X-Partner-Key header.

GET/v1/partner/meX-Partner-Key
Returns your partner account profile including plan details, usage counters, and limits.

Response 200

{
  "partnerId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "company": "Acme MSP",
  "plan": "professional",
  "planName": "Professional",
  "billing": "monthly",
  "status": "active",
  "billingMonth": "2026-04",
  "runsUsed": 42,
  "runLimit": 100,
  "customerCount": 18,
  "distributorId": null,
  "billedByDistributor": false,
  "keys": [
    { "slot": 1, "suffix": "abcd" }
  ]
}

Notes

Added in v2.1.8: keys array surfaces populated API key slots so dashboards can render the actual list (max 2). Each entry is {slot, suffix} — slot is 1 or 2 (max 2 active keys per partner), suffix is the last 4 chars of the raw key for visual identification only. Falls back to an empty array if no keys exist (shouldn't happen for an authenticated partner). Pre-v2.1.8 responses omit the field entirely.

POST/v1/partner/planX-Partner-Key
Upgrade or downgrade your MSP subscription plan. Changes take effect immediately on your Stripe subscription with proration — upgrades are charged on the next invoice, downgrades credit the unused portion of the current cycle. The new plan's run limit and allowed products apply to assessments triggered after the response returns.

Request body

{
  "plan": "professional",
  "billing": "monthly",
  "currency": "usd"
}

plan is required ('starter' | 'professional' | 'enterprise'). billing ('monthly' | 'rolling') and currency ('usd' | 'aud' | 'gbp' | 'eur' | 'sgd') are optional and default to your current values.

Response 200

{
  "partnerId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "plan": "professional",
  "billing": "monthly",
  "currency": "usd",
  "runLimit": 100,
  "subscriptionStatus": "active",
  "currentPeriodEnd": "2026-05-09T00:00:00Z"
}

Errors

400 INVALID_JSON           — request body could not be parsed
400 PLAN_CHANGE_FAILED     — invalid plan/billing, no active subscription, already on target plan,
                          or subscription is in a state that cannot be modified (canceled, etc)
401 Missing or invalid X-Partner-Key
500 Price lookup or Stripe API error

Rate-limited to 10 plan changes per hour per partner. Upgrades/downgrades are audited. Dashboard UI is available on the Billing tab of the partner portal.

GET/v1/partner/productsX-Partner-Key
Returns the list of products available on your current plan.

Response 200

{
  "products": [
    {
      "id": "essential-eight",
      "name": "Essential Eight Assessment",
      "category": "compliance",
      "estimatedDuration": 180
    },
    {
      "id": "nist-csf",
      "name": "NIST CSF Assessment",
      "category": "compliance",
      "estimatedDuration": 240
    }
  ]
}
GET/v1/partner/brandingX-Partner-Key
Returns your current partner branding (used in customer-facing emails and the report viewer). Available v2.1.8+.

Response 200

{
  "company": "Acme Cloud Security",
  "name": "Acme MSP",
  "brandColor": "#00cc66",
  "footerText": "© 2026 Acme Cloud Security",
  "contactEmail": "security@acme.com",
  "logoBlobPath": "partners/acme-msp/logo.svg",
  "logoUrl": "https://<account>.blob.core.windows.net/branding/partners/acme-msp/logo.svg?sv=...&sig=..."
}

logoUrl (added v2.1.9+) is a freshly-minted 30-day SAS URL for the uploaded logo blob — embed it directly in your own UIs. It is null when no logo has been uploaded or SAS minting failed (the underlying logoBlobPath stays available as a fallback). To upload or replace a logo, use the multipart endpoint POST /api/partner/branding/logo (MSAL only — partner dashboard).

PUT/v1/partner/brandingX-Partner-Key
Update partner branding text fields. All fields optional — provide only what you want to change. Drives customer-facing email rendering (consent invitation, report-ready, questionnaire) and the report viewer chrome. Available v2.1.8+.

Request body

{
  "brandColor": "#00cc66",
  "footerText": "© 2026 Acme Cloud Security",
  "contactEmail": "security@acme.com",
  "company": "Acme Cloud Security"
}

Response 200

{ "updated": true, "fields": 3 }

Errors

400 INVALID_JSON      — request body could not be parsed
400 INVALID_COLOR     — brandColor isn't a 6-digit hex (e.g. #00cc66)
400 INVALID_FOOTER    — footerText > 500 chars
400 INVALID_EMAIL     — contactEmail not a valid email address
400 INVALID_COMPANY   — company > 200 chars
400 NO_FIELDS         — body had no recognised fields to update
401 Missing or invalid X-Partner-Key
GET/v1/partner/customersX-Partner-Key
Returns all customers belonging to your partner account.

Response 200

{
  "customers": [
    {
      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "name": "Contoso Ltd",
      "tenantId": "d4f5a678-1234-5678-9abc-def012345678",
      "email": "it@contoso.com",
      "createdAt": "2026-02-10T14:00:00Z"
    }
  ]
}
POST/v1/partner/customersX-Partner-Key
Creates a new customer record linked to your partner account.

Request Body

FieldTypeDescription
namestringrequiredDisplay name for the customer organisation.
tenantIdstring (GUID)requiredMicrosoft 365 tenant ID for the customer.
emailstringoptionalPrimary contact email address.
receiversstring[]optionalEmail addresses that receive assessment report notifications.
questionnaireRecipientsstring[]optionalEmail addresses that receive questionnaire invitations.
emailReportsEnabledbooleanoptionalDefault true. Set to false to suppress all report-completion emails for this customer.
emailConsentEnabledbooleanoptionalDefault true. Set to false to suppress consent-invitation emails for this customer.
emailQuestionnaireEnabledbooleanoptionalDefault true. Set to false to suppress questionnaire-link emails for this customer.

Two-stage email gate (v2.1.5+): outbound mail to a customer is only sent when (1) your partner mail sender under /portal/branding is NOT set to off, AND (2) the customer's per-channel toggle above is not false. Stage 1 always wins. Set the partner-level mail provider to off for a brand-wide kill-switch; use the per-customer toggles for individual exemptions.

{
  "name": "Contoso Ltd",
  "tenantId": "d4f5a678-1234-5678-9abc-def012345678",
  "email": "it@contoso.com",
  "receivers": ["ciso@contoso.com", "it@contoso.com"],
  "questionnaireRecipients": ["ciso@contoso.com"],
  "emailReportsEnabled": true,
  "emailConsentEnabled": true,
  "emailQuestionnaireEnabled": true
}

Response 201

{
  "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "name": "Contoso Ltd",
  "tenantId": "d4f5a678-1234-5678-9abc-def012345678",
  "email": "it@contoso.com",
  "receivers": ["ciso@contoso.com", "it@contoso.com"],
  "questionnaireRecipients": ["ciso@contoso.com"],
  "createdAt": "2026-04-03T10:00:00Z"
}

curl Example

curl -X POST https://api.baref00t.io/api/v1/partner/customers \
  -H "X-Partner-Key: pk_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Contoso Ltd",
    "tenantId": "d4f5a678-1234-5678-9abc-def012345678",
    "email": "it@contoso.com",
    "receivers": ["ciso@contoso.com"],
    "questionnaireRecipients": ["ciso@contoso.com"]
  }'
GET/v1/partner/customers/{id}X-Partner-Key
Returns full details for a single customer including receiver lists and recent assessment history.

Response 200

{
  "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "name": "Contoso Ltd",
  "tenantId": "d4f5a678-1234-5678-9abc-def012345678",
  "email": "it@contoso.com",
  "receivers": ["ciso@contoso.com", "it@contoso.com"],
  "questionnaireRecipients": ["ciso@contoso.com"],
  "recentRuns": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "product": "essential-eight",
      "status": "completed",
      "completedAt": "2026-03-28T14:22:00Z"
    }
  ],
  "createdAt": "2026-02-10T14:00:00Z"
}
PUT/v1/partner/customers/{id}X-Partner-Key
Updates an existing customer. Send only the fields you want to change. Omitted fields remain unchanged.

Request Body

{
  "name": "Contoso Corporation",
  "receivers": ["ciso@contoso.com", "security@contoso.com"]
}

Response 200

{
  "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "name": "Contoso Corporation",
  "tenantId": "d4f5a678-1234-5678-9abc-def012345678",
  "email": "it@contoso.com",
  "receivers": ["ciso@contoso.com", "security@contoso.com"],
  "questionnaireRecipients": ["ciso@contoso.com"],
  "updatedAt": "2026-04-03T10:05:00Z"
}
DELETE/v1/partner/customers/{id}X-Partner-Key
Permanently removes a customer and all associated assessment data. This action cannot be undone.

Response 204

No content. The customer has been deleted.

POST/v1/partner/assessmentsX-Partner-Key
Triggers a new assessment run for a customer. The run executes asynchronously; poll the status endpoint or use webhooks to track progress.

Request Body

FieldTypeDescription
customerIdstringrequiredThe customer to run the assessment against.
productstringrequiredProduct identifier (e.g. essential-eight, nist-csf).
maturityTargetnumberoptionalTarget maturity level (1-5). Defaults to the product default.
attestationsobjectoptionalProcedural-attestation answers for products that carry a procedural questionnaire (e8, ransomware, cyber-essentials, mcsb, mas-trm, nis2). Map of questionId: boolean. A true answer upgrades the matching control's automated warn verdict to pass and records the attestation in the report audit trail. Catalogue: PROCEDURAL_QUESTIONS_BY_SLUG exported from @baref00t/assessments/attestation-questions. Ignored for products without a procedural questionnaire. (v2.4.6+, #526)
{
  "customerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "product": "essential-eight",
  "maturityTarget": 3,
  "attestations": {
    "e8_backup_restore_drills": true,
    "e8_backup_3_2_1": true,
    "e8_app_control_servers": false
  }
}

Response 202 — assessment queued

{
  "assessmentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "runId": "abc-...",
  "product": "essential-eight",
  "status": "queued",
  "runsUsed": 7,
  "runLimit": 100
}

Response 200 — consent required (v2.1.7+)

When the customer hasn't granted Microsoft admin consent on their tenant yet (no prior completed or running run for this customer), no run is created and no quota is burned. Share the returned consentUrl with the customer (or use POST /v1/partner/customers/{id}/send-consent to email it on the partner's behalf), then re-trigger.

{
  "status": "consent_required",
  "assessmentId": null,
  "consentUrl": "https://www.baref00t.io/consent?product=essential-eight&assessment_id=...&partner_id=...",
  "customerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "product": "essential-eight",
  "message": "Customer has not granted Microsoft admin consent on their tenant. Share the consentUrl with them, then re-trigger."
}
GET/v1/partner/assessmentsX-Partner-Key
Returns a list of assessment runs. Use query parameters to filter results.

Query Parameters

ParameterTypeDescription
monthstringoptionalFilter by month in YYYY-MM format.
productstringoptionalFilter by product identifier.
customerIdstringoptionalFilter by customer ID.

Response 200

{
  "runs": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "customerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "product": "essential-eight",
      "status": "completed",
      "score": 72,
      "createdAt": "2026-04-03T10:10:00Z",
      "completedAt": "2026-04-03T10:13:22Z"
    }
  ]
}
GET/v1/partner/assessments/{id}X-Partner-Key
Returns the current status and results of a single assessment run.

Response 200

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "customerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "customerName": "Contoso Ltd",
  "product": "essential-eight",
  "maturityTarget": 3,
  "status": "completed",
  "score": 72,
  "findings": 14,
  "critical": 2,
  "high": 5,
  "medium": 4,
  "low": 3,
  "reportUrl": "https://api.baref00t.io/api/r/{token}",
  "reportToken": "rT7xQz8kPnH2vL4w",
  "createdAt": "2026-04-03T10:10:00Z",
  "completedAt": "2026-04-03T10:13:22Z"
}

Customer-share URLs (v2.1.9+)

When status === "completed", the response includes a public reportToken. Build a customer-shareable short URL on your own portal domain as https://<your-portal-domain>/r/{reportToken} — the portal route 302-redirects to the platform's branded report viewer at https://www.baref00t.io/r/{token}. Anyone with the token can view the report (rate-limited per IP); rotate by re-running the assessment if a token leaks.

DELETE/v1/partner/assessments/{id}X-Partner-Key
Permanently delete an assessment run. Removes the run record, any linked proposal / narrative rows, AND the rendered report blobs — the customer's report link (and any /r/{token} short URL) stops working. Irreversible; there is no soft-delete. Ownership is enforced: deleting an assessment that does not belong to your partner account returns 404. Does NOT refund the run credit.

Response 200

{
  "deleted": true,
  "assessmentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "reportBlobsRemoved": 2
}

Response 404

Returned when the assessment id is unknown or belongs to a different partner.

{
  "error": { "code": "NOT_FOUND", "message": "Assessment not found" }
}
GET/v1/partner/assessments/{id}/reportX-Partner-Key
Returns a 302 redirect to a freshly-minted, time-limited (30-day) SAS URL for the rendered HTML report blob. Most HTTP clients follow the redirect automatically. The SDK exposes both getReport() (returns the URL) and a fetchReport() helper.

Query Parameters

ParameterTypeDescription
formatstringoptionalEither html (default) or pdf. PDF rendering is not yet supported and currently returns 501.

Response 302

HTTP/1.1 302 Found
Location: https://<account>.blob.core.windows.net/reports/<tenantId>/<assessmentId>.html?sv=...&sig=...
Cache-Control: no-store

Errors

StatusCodeMeaning
400INVALID_FORMATformat query value is neither html nor pdf
404NOT_FOUNDAssessment is not owned by this partner
404REPORT_NOT_READYAssessment exists but the report blob is not yet rendered
501NOT_IMPLEMENTEDformat=pdf — pipeline not yet wired
POST/v1/partner/customers/bulkX-Partner-Key
Creates multiple customers in a single request. Maximum 100 customers per request.

Request Body

{
  "customers": [
    { "name": "Contoso Ltd", "tenantId": "d4f5a678-1234-5678-9abc-def012345678", "email": "it@contoso.com" },
    { "name": "Fabrikam Inc", "tenantId": "a1b2c3d4-5678-9abc-def0-123456789abc", "email": "admin@fabrikam.com" }
  ]
}

Response 201

{
  "created": 2,
  "failed": 0,
  "customers": [
    { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "name": "Contoso Ltd", "status": "created" },
    { "id": "d4e5f6a7-b8c9-0123-defa-234567890123", "name": "Fabrikam Inc", "status": "created" }
  ]
}

Maximum batch size is 100 customers. Requests exceeding this limit return a TOO_MANY error.

POST/v1/partner/assessments/bulkX-Partner-Key
Triggers the same assessment product against multiple customers. Maximum 50 customers per request.

Request Body

{
  "product": "essential-eight",
  "customerIds": ["b2c3d4e5-f6a7-8901-bcde-f12345678901", "d4e5f6a7-b8c9-0123-defa-234567890123"],
  "maturityTarget": 3
}

Response 202

{
  "queued": 3,
  "failed": 0,
  "runs": [
    { "id": "f6a7b8c9-d0e1-2345-abcd-456789012345", "customerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "status": "queued" }
  ]
}

Maximum batch size is 50 customer IDs. Requests exceeding this limit return a TOO_MANY error.

POST/v1/partner/keysX-Partner-Key
Generates a new API key for your partner account. Each account can hold a maximum of 2 active keys.

Request Body

No request body required.

Response 201

{
  "slot": 2,
  "key": "pk_live_newkey789xyz",
  "createdAt": "2026-04-03T10:20:00Z"
}

The full key is only returned once at creation time. Store it securely. You cannot retrieve it again.

DELETE/v1/partner/keys/{slot}X-Partner-Key
Revokes the API key in the specified slot. The key becomes immediately invalid.

Path Parameters

ParameterTypeDescription
slotnumberrequiredKey slot to revoke: 1 or 2.

Response 204

No content. The key has been revoked.

You cannot revoke a key if it is the only active key on the account. Generate a new key first, then revoke the old one.

GET/api/partner/mail/statusX-Partner-Key or MSAL
Returns the current partner-mail provider configuration: which sender system is active, the connected Microsoft mailbox (if any), and whether the cached refresh token can still mint a Graph access token. Hybrid auth — both X-Partner-Key (SDK) and MSAL Bearer (hosted dashboard) work. Available v2.1.9+.

Response 200

{
  "provider": "microsoft",
  "connectedUserEmail": "admin@acme.com",
  "sharedMailbox": "noreply@acme.com",
  "hasValidToken": true,
  "tokenExpiresAt": "2026-05-02T14:30:00Z"
}

provider is 'resend' | 'microsoft' | 'off'. connectedUserEmail is null when provider isn't microsoft. sharedMailbox is null when not configured. hasValidToken: false means reconnect via /mail/connect-init.

Errors

401 Missing or invalid auth (X-Partner-Key or MSAL Bearer)
404 Partner record not found
PUT/api/partner/mail/modeX-Partner-Key or MSAL
Sets the partner-wide mail provider. 'off' disables ALL customer-facing email (kill-switch). 'resend' uses the platform's shared Resend transport. 'microsoft' sends from your tenant — requires a prior /mail/connect-init flow. MSAL callers must have Admin role; X-Partner-Key callers are always treated as Admin.

Request body

{ "mode": "microsoft" }

Response 200

{ "ok": true, "mode": "microsoft" }

Errors

400 Invalid JSON body
400 mode must be 'resend', 'microsoft', or 'off'
403 Admin role required to manage mail sending (MSAL only)
412 NO_CONNECTION — switching to 'microsoft' before /mail/connect-init has been completed
POST/api/partner/mail/disconnectX-Partner-Key or MSAL
Revokes the cached Microsoft refresh token and reverts the provider to Resend. Call this if the connected mailbox was compromised or the partner admin wants to re-consent under a different account. MSAL callers must have Admin role; X-Partner-Key callers are always treated as Admin.

Request body

None.

Response 200

{ "ok": true }
POST/api/partner/mail/testX-Partner-Key or MSAL
Sends a one-off test email via Microsoft Graph using the connected mailbox (or sharedMailbox if set). Recipient is always the calling user's own email — there is no body to specify. Use to confirm the OAuth flow + Graph permissions are wired correctly end-to-end. MSAL callers must have Admin role; X-Partner-Key callers are always treated as Admin.

Request body

None.

Response 200

{
  "delivered": true,
  "via": "microsoft",
  "recipient": "admin@acme.com",
  "fromMailbox": "noreply@acme.com"
}

Errors

403 Admin role required to manage mail sending (MSAL only)
412 NOT_CONNECTED — Microsoft mail sending isn't connected (or refresh failed)
502 GRAPH_<status> — Graph rejected the test send (check sharedMailbox UPN or reconnect)
502 GRAPH_NETWORK — network error reaching Graph
503 APP_NOT_CONFIGURED — platform's Partner Mail Sender Entra app isn't configured
PUT/api/partner/mail/shared-mailboxX-Partner-Key or MSAL
Set or clear the shared-mailbox UPN that customer-facing emails are sent FROM. When unset, mail is sent from the connected user's own mailbox. Pass null (or an empty string) to clear. The connected user must have Send-As permission on the shared mailbox in Exchange.

Request body

{ "sharedMailbox": "noreply@acme.com" }

Response 200

{ "ok": true, "sharedMailbox": "noreply@acme.com" }

Shared mailbox Sent Items routing: Exchange Online's default behaviour is to save sent-as-shared messages to the connected user's Sent Items, not the shared mailbox's. To save to both, run on your tenant: Set-Mailbox -Identity noreply@acme.com -MessageCopyForSentAsEnabled $true.

POST/v1/partner/mail/connect-initX-Partner-Key
Starts the Microsoft consent flow that authorises the multi-tenant 'baref00t Partner Mail Sender' Entra app to send mail in your tenant. Returns the Microsoft authorize URL — navigate the partner admin's browser to it; the OAuth callback persists tokens on the platform and bounces back to returnTo. Available v2.1.9+.

Request body

{ "returnTo": "https://portal.acme.com/settings/mail" }

returnTo must be HTTPS (or http://localhost:* for dev) and must not embed credentials. The state token is signed and partner-key-gated; no per-partner allowlist is enforced today.

Response 200

{ "authorizeUrl": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=...&state=..." }

Flow

  1. Partner navigates the browser to authorizeUrl.
  2. Microsoft consent prompt appears in their tenant.
  3. baref00t.io callback exchanges the auth code, persists access + refresh tokens, and 302-redirects back to state.returnTo with ?mailConnected=1 on success or ?mailError=<reason> on failure.
  4. After the callback returns, call PUT /api/partner/mail/mode with { "mode": "microsoft" } to switch the provider on.

Errors

400 BAD_RETURN_TO            — returnTo not HTTPS / localhost, or contains credentials
400 INVALID_JSON             — body could not be parsed
400 TENANT_UNSET             — partner record has no tenantId on file
503 MAIL_APP_NOT_CONFIGURED  — platform's Partner Mail Sender Entra app isn't configured
401 Missing or invalid X-Partner-Key
GET/api/partner/membersX-Partner-Key or MSAL
Lists all members on the partner account, plus the calling principal's effective role. X-Partner-Key callers always see callerRole = 'Admin' and callerEmailHash = '*' (the root partner key implies full trust). MSAL Bearer callers see their per-user role. Available v2.1.9+.

Response 200

{
  "members": [
    {
      "emailHash": "f1c2…",
      "email": "alice@acme.com",
      "role": "Admin",
      "status": "Active",
      "invitedAt": "2026-04-12T10:00:00Z",
      "acceptedAt": "2026-04-12T10:05:33Z",
      "inviteExpires": null,
      "hasOpenInvite": false,
      "createdAt": "2026-04-12T10:00:00Z",
      "updatedAt": "2026-04-12T10:05:33Z"
    }
  ],
  "callerRole": "Admin",
  "callerEmailHash": "*"
}

emailHash is a stable HMAC of the lowercased email — use it as the path id for member ops. email is the decrypted email; empty string when decryption failed (legacy rows). role is 'Admin' | 'Member' | 'Viewer'. status is 'Pending' | 'Active' | 'Suspended'.

POST/api/partner/membersX-Partner-Key or MSAL
Invites a new member by email + role. Sends an invitation email with a one-time accept link valid for 14 days. Re-inviting a Pending member resets the expiry; inviting an existing Active member returns 409. Admin role required (MSAL only — X-Partner-Key callers are always Admin).

Request body

{
  "email": "bob@acme.com",
  "role": "Member"
}

Response 201

{ "member": {  } }

Errors

400 Invalid JSON body
400 Valid email required
400 role must be Admin, Member, or Viewer
403 Admin role required (MSAL)
409 Member already exists / open invite present
PUT/api/partner/members/{emailHash}X-Partner-Key or MSAL
Update a member's role and/or status (provide at least one field). Status transitions to 'Pending' aren't user-initiated — only Active and Suspended are accepted. The platform routes both PUT and PATCH to the same handler so MSAL clients can keep using PATCH while SDK callers use PUT. Admin role required (MSAL only).

Request body

{
  "role": "Admin",
  "status": "Suspended"
}

Response 200

{ "member": {  } }

Errors

400 emailHash required
400 Invalid JSON body
400 Invalid role
400 status must be Active or Suspended
400 Provide role or status to update
403 Admin role required (MSAL)
404 Member not found
409 Conflict (e.g. demoting the last Admin)
DELETE/api/partner/members/{emailHash}X-Partner-Key or MSAL
Permanently removes a member from the partner account. Audit history (logs of past API calls / report views) is retained but the row is gone — re-add via invite. Admin role required (MSAL only). Cannot remove the last Admin.

Response 200

{ "ok": true }

Removal is permanent. The audit trail of past actions remains, but the member record (and any pending invite) is deleted.

POST/api/partner/members/{emailHash}/resendX-Partner-Key or MSAL
Re-issues the invite email and resets the 14-day accept window. Only valid for members in Pending status. Admin role required (MSAL only).

Response 200

{ "ok": true }
GET/api/partner/billingX-Partner-Key or MSAL
Read-only snapshot of the partner's Stripe billing state — active subscriptions, recent invoices (last 20), recent successful charges (last 20), plus a freshly-minted Stripe Customer Portal URL valid for ~1 hour. Returns empty arrays when the partner has no Stripe customer record yet. Available v2.1.9+.

Response 200

{
  "subscriptions": [
    {
      "id": "sub_1Q...",
      "status": "active",
      "product": "essential-eight",
      "plan": "professional",
      "amount": 49900,
      "currency": "aud",
      "interval": "month",
      "interval_count": 1,
      "current_period_start": "2026-04-01T00:00:00Z",
      "current_period_end": "2026-05-01T00:00:00Z",
      "cancel_at_period_end": false,
      "created": "2026-01-15T10:00:00Z"
    }
  ],
  "invoices": [
    {
      "id": "in_1Q...",
      "number": "BAREFOOT-0042",
      "status": "paid",
      "amount_due": 49900,
      "amount_paid": 49900,
      "currency": "aud",
      "invoice_pdf": "https://stripe.com/.../invoice.pdf",
      "hosted_invoice_url": "https://invoice.stripe.com/i/...",
      "period_start": "2026-04-01T00:00:00Z",
      "period_end": "2026-05-01T00:00:00Z",
      "created": "2026-04-01T00:00:00Z"
    }
  ],
  "charges": [
    {
      "id": "ch_1Q...",
      "amount": 49900,
      "currency": "aud",
      "description": "Professional · monthly",
      "receipt_url": "https://pay.stripe.com/receipts/...",
      "created": "2026-04-01T00:00:05Z"
    }
  ],
  "portal_url": "https://billing.stripe.com/p/session/..."
}

When the partner has no Stripe customer linked yet (e.g. still on a free trial), all three arrays are empty and portal_url is null. Use POST /api/partner/billing-portal to mint a guaranteed-fresh portal session at click time.

POST/api/partner/billing-portalX-Partner-Key or MSAL
Mints a fresh, short-lived Stripe Customer Portal URL. Open in a new tab — the partner manages payment methods, downloads invoices, and updates billing details directly on Stripe (no card data ever touches baref00t.io). Use this when the cached portal_url from GET /partner/billing has expired.

Request body

None.

Response 200

{ "url": "https://billing.stripe.com/p/session/test_..." }

Errors

400 No billing account linked (partner has no Stripe customer record)
401 Missing or invalid auth
500 Payment not configured
500 Failed to create billing session
GET/v1/partner/webhooksX-Partner-Key
Lists all webhook endpoints configured on this partner account, plus the catalog of event types you can subscribe to.

Response 200

{
  "endpoints": [{
    "id": "wh_01HX...",
    "url": "https://api.acme.com/baref00t/webhooks",
    "events": ["assessment.completed", "customer.created"],
    "enabled": true,
    "description": "prod handler",
    "secretPreview": "whsec_…X4iY",
    "createdAt": "2026-04-12T10:00:00Z",
    "lastSuccessAt": "2026-04-30T22:11:04Z",
    "lastFailureAt": null,
    "failureCount": 0
  }],
  "total": 1,
  "availableEvents": [
    "assessment.completed", "assessment.failed",
    "customer.created", "customer.updated", "customer.deleted",
    "lead.created", "lead.enriched", "lead.outreach_sent",
    "lead.stage_changed", "lead.consented", "lead.report_delivered"
  ]
}
POST/v1/partner/webhooksX-Partner-Key
Creates a new webhook endpoint. The signing secret is returned ONCE in the create response — store it; it cannot be re-fetched. Use it with the SDK's verifyWebhookSignature() to validate incoming deliveries.

Request Body

{
  "url": "https://api.acme.com/baref00t/webhooks",
  "events": ["assessment.completed"],
  "description": "prod handler"
}

Response 201

{
  "endpoint": {  },
  "secret": "whsec_<base64url>"
}

Errors

StatusCodeMeaning
400INVALID_URLurl is missing, not https, or targets localhost
400INVALID_EVENTSevents array is missing or empty
400UNSUPPORTED_EVENTevents contains a value not in availableEvents

The full secret is only returned at creation time. Store it securely — you cannot retrieve it again. Use update { rotateSecret: true } to rotate.

PUT/v1/partner/webhooks/{id}X-Partner-Key
Partial update — set any combination of url, events, enabled, description, or rotateSecret: true (returns a new secret in the response, the old one stops working immediately).

Request Body

{
  "enabled": false,
  "rotateSecret": true
}

Response 200

{
  "endpoint": {  },
  "secret": "whsec_<new>"
}
DELETE/v1/partner/webhooks/{id}X-Partner-Key
Removes the endpoint. No further deliveries are attempted; historical delivery rows remain queryable until the retention window expires.

Response 200

{ "deleted": true, "endpointId": "wh_01HX..." }
POST/v1/partner/webhooks/{id}/testX-Partner-Key
Fires a synthetic test.ping delivery to the endpoint with a real signed payload — useful while wiring up your handler. The 10-second response is returned inline so you can see exactly what your server replied.

Response 200 (delivered)

{
  "delivered": true,
  "statusCode": 200,
  "durationMs": 142,
  "lastError": null,
  "responseBody": "ok"
}

Signature header

X-Baref00t-Signature: t=<unix_seconds>,v1=<hex_sha256>

HMAC-SHA256 over ${timestamp}.${rawBody} using the endpoint secret. Use verifyWebhookSignature() from @baref00t/sdk/webhooks to validate.

GET/v1/partner/webhooks/{id}/deliveriesX-Partner-Key
Recent delivery attempts for the endpoint. Useful for debugging when a partner's handler is failing — surfaces statusCode, durationMs, lastError, and a 500-byte slice of the response body.

Query Parameters

ParameterTypeDescription
limitnumberoptional1–200, default 50.

Response 200

{
  "deliveries": [{
    "id": "del_01HX...",
    "eventType": "assessment.completed",
    "status": "Delivered",
    "statusCode": 200,
    "attemptCount": 3,
    "durationMs": 142,
    "lastError": null,
    "deliveredAt": "2026-04-30T22:35:04Z",
    "nextRetryAt": null,
    "createdAt": "2026-04-30T22:11:04Z"
  }],
  "total": 1,
  "limit": 50
}

Delivery retries (v2.1.7+)

Failed deliveries are retried automatically with exponential backoff. attemptCount tracks the current attempt number; status can be Delivered, Failed (still retrying), or Abandoned (max attempts exceeded). Schedule:

AttemptWhenFrom event creation
1Inline (when event fires)0
2+1 minute~1 min
3+5 minutes~6 min
4+30 minutes~36 min
5+2 hours~2.5 hr
6+24 hours~26.5 hr

A retry is considered successful on any HTTP 2xx response within 10 seconds. After attempt 6 still failing, the delivery is marked Abandoned and no further retries occur — your handler must be idempotent and the event is dropped after that. Each retry sets the header X-Baref00t-Attempt: N and re-signs with the current timestamp (so partners' replay-window protection still works against the X-Baref00t-Timestamp header).

Partner — Pipeline (Leads)

Programmatic access to the partner prospecting pipeline — Apollo + Hunter contact enrichment, M365 OIDC tenant discovery, and the New → Enriched → OutreachSent → Consented → ReportDelivered → Won | Lost stage machine.

Every endpoint in this section requires the partner record to have prospecting in enabledFeatures. The flag is admin-granted only — no plan tier auto-includes it. Calling these endpoints without the flag returns 403 FEATURE_NOT_ENABLED.

Lead PII (primary contact name/email, contacts list, partner overrides) is encrypted at rest with AES-256-GCM since v2.2.7. Plaintext columns dropped in v2.2.13 — encrypted shadows are the sole source of truth on reads.

Customer materialisation lifecycle (#369)

Two flows coexist, gated by the per-partner lead-deferred-customer feature flag:

| Stage / event | Legacy flow (default) | Deferred-customer flow (lead-deferred-customer set) | |---------------------------|-----------------------------------------------|------------------------------------------------------------| | send-consent | partner_customer row created (or attached) | No customer created. partner_runs.lead_id populated. | | Consent grant (M365 OIDC) | partner_customer.tenant_id backfilled | partner_lead.tenant_id backfilled. Run stays orphan. | | Report delivered | Recipient = customer.receivers[] | Recipient = lead.primaryContactEmail. Stage-2 gate skipped (Stage-1 partner mail provider still applies). | | setStage('Won') | No-op on customer | partner_customer row created from lead; partner_runs.customer_id backfilled in same transaction. Fires lead.won + customer.created. Idempotent on re-Win. | | setStage('Lost') | Cancel cadence | Cancel cadence. No customer materialised. |

In both flows, the assessmentId returned by send-consent is the same id used throughout the assessment + report webhooks; the difference is purely when the partner_customer row exists. SDK callers should treat SendLeadConsentResponse.customerId as nullable.

POST/v1/partner/leadsX-Partner-Key
Create a single lead from a domain. Runs Apollo + Hunter enrichment plus an M365 OIDC probe synchronously by default so the response carries enriched fields.

Request body

{
  "domain": "acme.com",                  // required, bare domain. Protocol/path/www. stripped server-side
  "product": "e8",                        // optional product slug
  "maturityTarget": "level-2",            // optional product-specific level
  "notes": "Met at Sydney CSC meetup",   // optional, up to 5000 chars
  "enrich": true                          // default true; false to skip and call /enrich later
}

Response 201

{
  "lead": {
    "id": "uuid",
    "domain": "acme.com",
    "stage": "Enriched",
    "tenantId": "guid-or-null",
    "isOnM365": true,
    "primaryContactName": "Jane Doe",
    "primaryContactEmail": "jane@acme.com",
    "contacts": [ ],
    "createdAt": "...",
    "updatedAt": "..."
  },
  "enrichmentError": null
}

Errors

400 INVALID_DOMAIN          — domain didn't canonicalise
403 FEATURE_NOT_ENABLED     — partner lacks 'prospecting' flag
409 DUPLICATE               — lead exists for this domain. Response includes existingLeadId.
POST/v1/partner/leads/bulkX-Partner-Key
Create up to 50 leads at once. Enrichment runs asynchronously in the background — poll lead.get or subscribe to the lead.enriched webhook event to learn when each finishes.

Request body

{
  "domains": ["acme.com", "globex.com", "initech.com"],
  "product": "e8",
  "maturityTarget": "level-2",
  "enrich": true
}

Response 202

{
  "created": [
    { "id": "uuid", "domain": "acme.com", "stage": "New" }
  ],
  "skipped": [
    { "domain": "globex.com", "reason": "duplicate", "existingId": "..." }
  ],
  "enrichmentQueued": 2
}

skipped[].reason is "duplicate" (existing lead for this partner) or "invalid" (domain didn't canonicalise).

GET/v1/partner/leadsX-Partner-Key
Paginated list with stage-count rollup. Filter by stage, search by domain or company name.

Query parameters

ParameterTypeRequiredNotes
stagestringoptionalSingle stage filter.
searchstringoptionalSubstring on domain or company name (case-insensitive on company).
limitnumberoptionalDefault 50, max 200.
cursorstringoptionalOpaque cursor returned as nextCursor in the prior page.

Response 200

{
  "leads": [{ "id": "...", "domain": "...", "stage": "Enriched" }],
  "nextCursor": "2026-05-04T12:00:00.000Z",
  "counts": {
    "New": 14, "Enriched": 8, "OutreachSent": 3,
    "Consented": 1, "ReportDelivered": 0, "Won": 0, "Lost": 2
  },
  "total": 28
}
GET/v1/partner/leads/{leadId}X-Partner-Key
Detail + most recent 50 activity log entries.

Response 200

{
  "lead": { },
  "activities": [
    { "id": "...", "type": "enriched", "detail": "Enriched from apollo, hunter, m365",
      "metadata": { "sources": ["apollo","hunter","m365"], "hasTenantId": true },
      "actorEmail": null, "createdAt": "..." }
  ]
}
PATCH/v1/partner/leads/{leadId}X-Partner-Key
Partial update of whitelisted fields. Stage is NOT settable here — use /stage for terminal Won/Lost; other stages advance server-side.

Settable fields

companyName, industry, sizeBand, revenueBand, primaryContactName, primaryContactEmail, primaryContactRole, product, maturityTarget, notes, lostReason, partnerPrimaryOverride, partnerContactOverrides.

Request body example

{
  "partnerPrimaryOverride": {
    "name": "Jane Doe",
    "email": "jane@acme.com",
    "role": "CTO"
  },
  "notes": "Spoke at the Sydney meetup; warm intro from John."
}

Pass partnerPrimaryOverride: null to revert to the enrichment-picked primary. Pass partnerContactOverrides: null to clear all per-contact edits.

Response 200

{ "lead": {  } }
DELETE/v1/partner/leads/{leadId}X-Partner-Key
Soft-delete. Cancels any in-flight outreach cadence. Re-creating the same domain produces a fresh lead with a new id.

Response 200

{ "deleted": true }
POST/v1/partner/leads/{leadId}/enrichX-Partner-Key
Re-run enrichment: Apollo people search + Hunter domain search + Microsoft 365 OIDC tenant discovery. Pass force=true to bypass the freshness cache.

Request body (optional)

{ "force": false }

Response 200

{
  "lead": {  },
  "sources": ["apollo", "hunter", "m365"],
  "fromCache": false
}

On successful enrichment a New lead auto-advances to Enriched and the lead.enriched webhook fires (cache hits suppressed).

POST/v1/partner/leads/{leadId}/stageX-Partner-Key
Set a terminal stage. Only Won and Lost are partner-settable — intermediate stages advance automatically as enrichment finishes and the assessment progresses.

Request body

{
  "stage": "Lost",
  "lostReason": "Customer chose competitor"
}

Setting Won or Lost cancels any in-flight outreach cadence and fires lead.stage_changed.

Errors

400 INVALID_STAGE     — tried to set a non-terminal stage
400 MISSING_FIELD     — stage='Lost' without lostReason
GET/v1/partner/leads/{leadId}/activityX-Partner-Key
Paginated activity log per lead. Activity types include created, enriched, enrichment_failed, email_sent, email_delivered, email_opened, email_clicked, email_bounced, consented, report_ready, stage_changed, note.

Query parameters

limit  — default 50, max 200
cursor — opaque cursor from a prior call

Response 200

{
  "activities": [
    { "id": "...", "type": "email_sent", "detail": "Consent email sent to ciso@acme.com",
      "metadata": { "touchNumber": 1, "recipient": "ciso@acme.com" },
      "actorEmail": "partner@msp.com", "createdAt": "..." }
  ]
}

Distributor API

The Distributor API lets you provision and manage multiple partner accounts under a single billing relationship. All endpoints require the X-Distributor-Key header.

GET/v1/distributor/meX-Distributor-Key
Returns your distributor account profile including plan details and aggregate usage.

Response 200

{
  "id": "dist_xyz789",
  "company": "Global Security Distribution",
  "email": "ops@globalsecdist.com",
  "status": "active",
  "partnersCount": 12,
  "usage": {
    "month": "2026-04",
    "totalRuns": 384,
    "totalCustomers": 156
  },
  "createdAt": "2025-11-01T08:00:00Z"
}
GET/v1/distributor/partnersX-Distributor-Key
Returns all sub-partner accounts under your distributor account.

Query Parameters

ParameterTypeDescription
statusstringoptionalFilter by status: active, suspended, pending.

Response 200

{
  "partners": [
    {
      "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
      "company": "Acme MSP",
      "email": "admin@acmemsp.com",
      "plan": "professional",
      "status": "active",
      "customersCount": 18,
      "createdAt": "2026-01-15T09:30:00Z"
    }
  ]
}
POST/v1/distributor/partnersX-Distributor-Key
Provisions a new partner account under your distributor. The partner receives an activation email.

Request Body

FieldTypeDescription
emailstringrequiredAdmin email for the new partner account.
companystringrequiredCompany name for the partner.
namestringrequiredContact name for the partner admin.
planstringrequiredPlan to assign: starter, professional, enterprise.
billingobjectrequiredBilling configuration for the partner.
billing.currencystringrequiredISO 4217 currency code: AUD, USD, GBP, EUR, SGD.
billing.intervalstringoptionalBilling interval: monthly (default), quarterly, or annual.
{
  "email": "admin@newmsp.com",
  "company": "New MSP Pty Ltd",
  "name": "Jane Smith",
  "plan": "professional",
  "billing": {
    "currency": "AUD",
    "interval": "monthly"
  }
}

Response 201

{
  "id": "prt_new456",
  "company": "New MSP Pty Ltd",
  "email": "admin@newmsp.com",
  "plan": "professional",
  "status": "pending",
  "billing": {
    "currency": "AUD",
    "interval": "monthly"
  },
  "createdAt": "2026-04-03T11:00:00Z"
}
GET/v1/distributor/partners/{id}X-Distributor-Key
Returns full details for a sub-partner including current plan, billing, and usage for the current month.

Path Parameters

ParameterTypeDescription
idstringrequiredPartner ID (e.g. c3d4e5f6-a7b8-9012-cdef-123456789012).

Response 200

{
  "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "company": "Acme MSP",
  "email": "admin@acmemsp.com",
  "name": "John Doe",
  "plan": "professional",
  "status": "active",
  "billing": { "currency": "AUD", "interval": "monthly" },
  "usage": {
    "month": "2026-04",
    "runsUsed": 42,
    "runsLimit": 200,
    "customersCount": 18
  },
  "createdAt": "2026-01-15T09:30:00Z"
}
PUT/v1/distributor/partners/{id}X-Distributor-Key
Updates a sub-partner's plan or billing configuration. Send only the fields you want to change.

Request Body

{
  "plan": "enterprise",
  "billing": { "interval": "annual" }
}

Response 200

{
  "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "plan": "enterprise",
  "billing": { "currency": "AUD", "interval": "annual" },
  "updatedAt": "2026-04-03T11:15:00Z"
}
POST/v1/distributor/partners/{id}/activateX-Distributor-Key
Activates a pending or suspended partner account. The partner immediately gains access to the API and portal.

Request Body

No request body required.

Response 200

{
  "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "status": "active",
  "activatedAt": "2026-04-03T11:20:00Z"
}
POST/v1/distributor/partners/{id}/suspendX-Distributor-Key
Suspends an active partner account. The partner's API keys are immediately invalidated and portal access is revoked.

Request Body

No request body required.

Response 200

{
  "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "status": "suspended",
  "suspendedAt": "2026-04-03T11:25:00Z"
}

Suspending a partner immediately invalidates all of their API keys. The partner will need to generate new keys after reactivation.

POST/v1/distributor/partners/{id}/planX-Distributor-Key
Upgrade or downgrade a sub-partner's MSP subscription plan on their behalf. The partner's Stripe subscription is updated with prorated billing, and the new plan's run limits apply immediately.

Request body

{
  "plan": "enterprise",
  "billing": "rolling",
  "currency": "aud"
}

Response 200

{
  "partnerId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "plan": "enterprise",
  "billing": "rolling",
  "currency": "aud",
  "runLimit": -1,
  "subscriptionStatus": "active",
  "currentPeriodEnd": "2027-04-09T00:00:00Z"
}

runLimit: -1 indicates unlimited.

Errors

404 NOT_FOUND              — partner does not exist or is not owned by this distributor
400 PLAN_CHANGE_FAILED     — invalid plan/billing, no active subscription, already on target plan,
                          or subscription is in a state that cannot be modified
401 Missing or invalid X-Distributor-Key
500 Price lookup or Stripe API error

Plan changes against sub-partners are audited under the distributor's ID. If the distributor has StripeBillingEnabled=false, the partner's own Stripe subscription is still updated — revenue-share settlement is tracked separately.

GET/v1/distributor/partners/{id}/customersX-Distributor-Key
Returns all customers belonging to a specific sub-partner.

Response 200

{
  "customers": [
    {
      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "name": "Contoso Ltd",
      "tenantId": "d4f5a678-1234-5678-9abc-def012345678",
      "email": "it@contoso.com",
      "createdAt": "2026-02-10T14:00:00Z"
    }
  ]
}
POST/v1/distributor/partners/{id}/customersX-Distributor-Key
Creates a customer record under a specific sub-partner. The request body is identical to the Partner API's create customer endpoint.

Request Body

{
  "name": "Contoso Ltd",
  "tenantId": "d4f5a678-1234-5678-9abc-def012345678",
  "email": "it@contoso.com",
  "receivers": ["ciso@contoso.com"],
  "questionnaireRecipients": ["ciso@contoso.com"]
}

Response 201

{
  "id": "cust_new789",
  "name": "Contoso Ltd",
  "tenantId": "d4f5a678-1234-5678-9abc-def012345678",
  "email": "it@contoso.com",
  "receivers": ["ciso@contoso.com"],
  "questionnaireRecipients": ["ciso@contoso.com"],
  "partnerId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "createdAt": "2026-04-03T11:30:00Z"
}

v2.1.5 (#309): distributor assessment endpoints removed. Distributors manage partners + customers + view aggregated usage. Per-customer assessment trigger and run listing is partner-key only — distributors must not see or perform per-customer assessment work on behalf of their downstream partners without the partner's own consent. Migrate aggregate roll-up to GET /v1/distributor/usage; per-run / per-customer detail must use the partner's own pk_live_… key against the Partner API.

GET/v1/distributor/usageX-Distributor-Key
Returns aggregate usage across all sub-partners for the specified month.

Query Parameters

ParameterTypeDescription
monthstringoptionalMonth in YYYY-MM format. Defaults to current month.

Response 200

{
  "month": "2026-04",
  "totalPartners": 12,
  "activePartners": 10,
  "totalCustomers": 156,
  "totalRuns": 384,
  "byPartner": [
    {
      "partnerId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
      "company": "Acme MSP",
      "runs": 42,
      "customers": 18
    },
    {
      "partnerId": "prt_def456",
      "company": "SecureIT Partners",
      "runs": 67,
      "customers": 24
    }
  ]
}
DELETE/v1/distributor/keys/{slot}X-Admin-Key
Revokes a distributor API key by slot number (1 or 2). Admin-only endpoint.

Path Parameters

ParameterTypeDescription
slotstringrequiredKey slot to revoke: 1 or 2.

Query Parameters

ParameterTypeDescription
distributorIdstringrequiredUUID of the distributor whose key to revoke.

Response 200

{ "revoked": true, "slot": 1 }
POST/v1/distributor/partners/bulkX-Distributor-Key
Provisions multiple partner accounts in a single request. Maximum 50 partners per request.

Request Body

{
  "partners": [
    {
      "email": "admin@mspalpha.com",
      "company": "MSP Alpha",
      "name": "Alice Wong",
      "plan": "professional",
      "billing": { "currency": "AUD", "interval": "monthly" }
    },
    {
      "email": "admin@mspbeta.com",
      "company": "MSP Beta",
      "name": "Bob Chen",
      "plan": "starter",
      "billing": { "currency": "USD", "interval": "monthly" }
    }
  ]
}

Response 201

{
  "created": 2,
  "failed": 0,
  "partners": [
    { "id": "prt_alpha01", "company": "MSP Alpha", "status": "pending" },
    { "id": "prt_beta02", "company": "MSP Beta", "status": "pending" }
  ]
}

Maximum batch size is 50 partners. Requests exceeding this limit return a TOO_MANY error.

TypeScript SDK

The official typed client for both APIs is published as @baref00t/sdk on npm. It wraps auth, retries, error shapes, and webhook signature verification — and tracks the API shape via a CI contract test against staging, so SDK and platform never drift.

Source: npmjs.com/package/@baref00t/sdk — Apache-2.0, no workspace deps, dual ESM + CJS.

Install

npm install @baref00t/sdk
# or
pnpm add @baref00t/sdk

Partner client

import { PartnerClient, BareF00tApiError } from '@baref00t/sdk/partner'

const client = new PartnerClient({ apiKey: process.env.BAREF00T_PARTNER_KEY! })

// List customers
const { customers, total } = await client.customers.list()

// Trigger an assessment
const run = await client.assessments.create({
  customerId: customers[0].customerId,
  product: 'tenant-health',
})

// Fetch the rendered report — returns the SAS URL without following the 302
const { url, expiresAt } = await client.assessments.getReport(run.assessmentId)

// Permanently delete an assessment + its report (irreversible, ownership-checked)
await client.assessments.delete(run.assessmentId)

// Manage webhooks
const ep = await client.webhooks.create({
  url: 'https://api.acme.com/baref00t/webhooks',
  events: ['assessment.completed', 'customer.created'],
})
console.log(ep.secret)  // store this — only returned ONCE

Pipeline (leads)

// Create a lead — sync enrichment by default
const { lead } = await client.leads.create({ domain: 'acme.com', product: 'e8' })

// Send the partner-branded outreach (costs 1 credit)
const sent = await client.leads.sendConsent(lead.id, {
  recipientEmails: ['ciso@acme.com'],
})
console.log('via', sent.recipients[0].via) // 'resend' or 'microsoft'

// Or — mint URLs without sending (still 1 credit)
const minted = await client.leads.sendConsent(lead.id, {
  recipientEmails: ['ciso@acme.com'],
  skipEmail: true,
})
const consentUrl = minted.recipients[0].consentUrl

Distributor client

import { DistributorClient } from '@baref00t/sdk/distributor'

const client = new DistributorClient({ apiKey: process.env.BAREF00T_DISTRIBUTOR_KEY! })
const { partners } = await client.partners.list()

Webhook verification

import { verifyWebhookSignature, BareF00tWebhookError } from '@baref00t/sdk/webhooks'

try {
  const event = verifyWebhookSignature({
    rawBody: req.body,                              // Buffer or string — NOT parsed JSON
    signature: req.header('X-Baref00t-Signature')!,
    secret: process.env.BAREF00T_WEBHOOK_SECRET!,
  })
  if (event.type === 'assessment.completed') {
    // event.data is typed
  }
} catch (err) {
  if (err instanceof BareF00tWebhookError) {
    // 'malformed' | 'invalid_signature' | 'timestamp_too_old'
    return res.status(400).end()
  }
  throw err
}

Errors: every non-2xx throws a typed BareF00t{Api,Network,Config,RateLimit,Webhook}Error subclass with status, code, and body fields. BareF00tRateLimitError exposes retryAfterSeconds for backoff.

MCP server

The baref00t MCP (Model Context Protocol) server exposes the full Partner + Distributor SDK surface as tools that AI assistants like Claude Desktop, Claude Code, and any MCP-aware client can call directly. Tools are scoped by the API key prefix you authenticate with — pk_* partner keys see partner tools, dk_* distributor keys see distributor tools.

| Environment | URL | |---|---| | prod | https://mcp.baref00t.io/mcp | | sandbox | https://mcp.sandbox.baref00t.io/mcp |

Authentication

OAuth (recommended for AI clients) — Claude Desktop / Claude.ai / Cursor / ChatGPT discover the auth surface automatically via /.well-known/oauth-authorization-server. Add the MCP URL and the client pops a browser to sign in via Microsoft. No JSON, no API key paste. Manage authorized apps at /portal/developer/connected-apps.

API key (server-to-server) — for SDK, CI, or your own backend, send Authorization: Bearer pk_live_… (or X-Partner-Key: pk_live_… / X-Distributor-Key: dk_live_…). Get keys at /portal/developer/api-keys.

Inspect

Before wiring into a desktop client, run the official MCP inspector:

# OAuth (pops browser for Microsoft sign-in)
npx -y @modelcontextprotocol/inspector https://mcp.baref00t.io/mcp

# API key (server-to-server)
npx -y @modelcontextprotocol/inspector https://mcp.baref00t.io/mcp \
  --header "X-Partner-Key: pk_live_..."

Available tools (51 total)

This page is autogenerated from the MCP registry — see scripts/gen-mcp-docs.mjs. Edits here will be overwritten by CI.

Customers (6)

ToolWhat it does
partner_list_customersList all customers in the partner account. Returns customerId, name, email, tenantId, status and createdAt. Use when the user asks "show me my customers" or wants to look up a customer id.
partner_get_customerFetch a single customer by id, including notification toggles + creation date. Use when the user asks for details about a specific customer.
partner_create_customerCreate a new customer record. Requires the customer's display name and Microsoft Entra tenant id (GUID). Returns the new customerId. Use when the user asks to add or onboard a customer.
partner_update_customerUpdate whitelisted fields on an existing customer. Partial — only fields you pass are changed. Note: changing tenantId rebinds the customer to a different Microsoft Entra tenant; existing consents tied to the old tenant will not transfer.
partner_delete_customerDelete a customer. Idempotent — re-deleting returns 404. Past assessments + reports tied to the customer remain queryable through partner_list_assessments / partner_get_assessment_report until their own retention windows expire; only the customer record itself is removed. Granted consents on the Microsoft Entra tenant are NOT revoked here — the customer admin must remove the baref00t app from Enterprise Applications separately.
partner_bulk_create_customersBulk-create up to 100 customers in one request. Per-row failures do NOT fail the whole call — inspect results[] for the per-customer outcome (status: "created" | "error", with customerId or error string). Returns total / succeeded / failed counts plus the per-row results array. Use partner_create_customer for one-offs; this is for onboarding spreadsheets / CSV imports.

Consent (2)

ToolWhat it does
partner_get_consent_urlMint a consent URL the customer can visit to grant the baref00t app read-only Microsoft Graph access on their tenant. Required before the first assessment for any customer who hasn't already consented — partner_trigger_assessment will silently fail (orchestrator graph token-exchange 401s) without consent. Returns { consentUrl, assessmentId, product, customerId }. Hand the URL to the partner to share with the customer manually, or use partner_send_consent_email to email it on the partner's behalf.
partner_send_consent_emailSend a partner-branded consent invitation email to the customer's configured consentRecipients (or primary email as fallback). The email contains a Grant Consent button linking to the consent URL. Returns { sent, skipped, recipients, failed, consentUrl, emailConsentEnabled, skipReason }. Honours the 2-stage email gate: if the partner's mail provider is 'off' OR the customer's emailConsentEnabled is false, no email is sent and emailConsentEnabled is returned as false with skipReason set; consentUrl is always returned so it can be shared manually.

Assessments (6)

ToolWhat it does
partner_list_assessmentsList assessment runs for the partner. Filter by month (YYYY-MM), product, and/or customerId. Also returns runsUsed/runLimit so the LLM knows how much quota remains. Use when the user asks "show me recent runs" or wants to find a specific assessment.
partner_get_assessmentFetch one assessment run by id. Returns product, status, customer id, and timestamps. Use to check the live status of a previously triggered run.
partner_get_assessment_reportGet a 30-day SAS URL for the rendered HTML report of a completed assessment. Returns { url, expiresAt }. Hand the URL to the user as a clickable link or fetch it in a browser. The assessment must be in completed status — call partner_get_assessment_status first to confirm. Only HTML format is supported today; PDF format returns NOT_IMPLEMENTED.
partner_trigger_assessmentStart a new assessment for the given customer. The platform performs a live consent pre-flight (client-credentials token mint against the customer tenant) before accepting the run. Inspect the response's "status" field: "queued"/"completed"/"failed" means the run was accepted (assessmentId + runId populated, COSTS 1 CREDIT — poll via partner_get_assessment_status). "consent_required" means the per-product Entra app does NOT currently exist in the customer tenant (never consented OR the customer admin removed it from Enterprise Applications) — no credit was burned, assessmentId is null, but consentUrl is returned. In that case call partner_send_consent_email or share consentUrl directly with the customer, then re-trigger.
partner_delete_assessmentPermanently delete one of your assessments. Removes the run record, any linked proposal / narrative rows, AND the rendered report (the customer’s report link stops working). Irreversible — there is no undo and no soft-delete. Ownership is enforced: deleting an assessment that is not yours returns 404. Use partner_list_assessments to find the id first.
partner_get_assessment_statusPoll the status of a previously-triggered assessment. Returns the current status (queued, completed, failed) and the runAt timestamp. Use to wait for a triggered assessment to finish.

Plan & Billing (3)

ToolWhat it does
partner_get_plan_billingFetch the partner's current plan, billing cycle, monthly assessment quota usage, and list of allowed products. Use when the user asks about their plan, quota, or available products before triggering an assessment.
partner_get_billingRead-only Stripe snapshot for the partner: active subscriptions, last 20 invoices, last 20 successful charges, plus a freshly-minted Stripe Customer Portal URL valid ~1 hour. When the partner has no Stripe customer linked yet, all arrays are empty and portal_url is null. For just the portal URL (or a guaranteed-fresh one) use partner_get_payment_portal_url. The platform owns the subscription lifecycle — plan changes still go through partner_get_plan_billing / the change-plan SDK call.
partner_get_payment_portal_urlMint a fresh, short-lived Stripe Customer Portal URL. The partner opens it in a new tab to manage payment methods, download invoices, and update billing details directly on Stripe — no card data ever touches baref00t.io. Use this when the cached portal_url from partner_get_billing has expired (or any time you want a guaranteed-fresh session). Errors 400 if the partner has no Stripe customer linked yet.

Branding (2)

ToolWhat it does
partner_get_brandingFetch the partner-wide branding (company name, brand colour, footer text, contact email, and a 30-day SAS URL for the uploaded logo when present). Drives customer-facing emails + the report viewer chrome. Use when the user asks "what does our branding look like?" or before mutating a single field via partner_update_branding.
partner_update_brandingUpdate one or more partner-branding text fields. All fields optional — provide only what you want to change. Affects customer-facing emails (consent invitation, report-ready, questionnaire) and the report viewer chrome. Logo upload is a separate multipart flow (POST /api/partner/branding/logo, MSAL only) and is not covered by this tool.

Members (5)

ToolWhat it does
partner_list_membersList all members on the partner account, with role (Admin / Member / Viewer), status (Pending / Active / Suspended), and lifecycle timestamps. Also returns the calling principal's effective role — for X-Partner-Key callers this is always "Admin". Each member row exposes an emailHash that downstream member tools (update / remove / resend) use as the path identifier.
partner_invite_memberInvite a new member to the partner account. Sends an invitation email with a one-time accept link valid for 14 days. Returns the new (Pending) member record including the emailHash needed for downstream update/remove/resend calls. Errors 409 if the address is already a member or has an open invite.
partner_update_memberUpdate a member's role and/or status (provide at least one). Suspending revokes their ability to sign in but retains the row for audit; reactivate by setting status="Active". Cannot demote the last Admin — errors 409 in that case.
partner_remove_memberWARNING — permanent. Removes a member from the partner account; the row is deleted (audit history of past actions is retained on the audit log, but the member entry is gone — re-add via partner_invite_member). Cannot remove the last Admin (errors 409). Prefer partner_update_member with status="Suspended" if you might re-enable later.
partner_resend_inviteRe-issue the invitation email and reset the 14-day accept window for a Pending member. Errors 409 if the member is not in Pending status (use partner_update_member to change role on already-Active members instead).

Webhooks (6)

ToolWhat it does
partner_list_webhooksList all webhook endpoints configured on the partner account, plus the catalog of event types you can subscribe to (assessment.completed / failed, customer.created / updated / deleted, etc.). Each row includes lastSuccessAt, lastFailureAt, and failureCount — handy for spotting endpoints that are silently broken.
partner_create_webhookCreate a new webhook endpoint. WARNING — the signing secret is returned ONCE in response.secret; store it immediately (e.g. in your secrets manager). It cannot be re-fetched. Use partner_update_webhook with rotateSecret=true to mint a fresh secret if lost. Verify incoming deliveries against X-Baref00t-Signature using verifyWebhookSignature() from @baref00t/sdk/webhooks.
partner_update_webhookPartial update for a webhook endpoint — change the URL, events, enabled flag, description, and/or rotate the signing secret. When rotateSecret=true, the new secret is returned ONCE in response.secret and the previous one stops working immediately.
partner_delete_webhookWARNING — permanent. Removes the webhook endpoint and STOPS any in-flight retries (events currently mid-backoff are abandoned). Historical delivery rows remain queryable until the retention window expires. If you only want to pause deliveries, prefer partner_update_webhook with enabled=false instead.
partner_test_webhookFire a synthetic test.ping delivery to the endpoint with a real signed payload — useful while wiring up your handler. The 10-second response is returned inline so you can see exactly what your server replied (statusCode + first ~500 bytes of responseBody). Does NOT cost a credit and does NOT count toward retry backoff.
partner_list_webhook_deliveriesRecent delivery attempts for a single webhook endpoint. Useful for debugging when a partner's receiver is silently failing — surfaces statusCode, durationMs, lastError, attemptCount (with v2.1.7+ retry-backoff schedule: 1m, 5m, 30m, 2h, 24h), and a 500-byte slice of the response body. Status can be Pending / Delivered / Failed / Abandoned.

Mail (6)

ToolWhat it does
partner_mail_statusFetch the current partner-mail configuration: provider ("resend" / "microsoft" / "off"), connected Microsoft mailbox UPN (when provider="microsoft"), shared-mailbox UPN, whether the cached refresh token can still mint a Graph access token, and the access-token expiry. Use to check status before mode/sharedMailbox changes or to decide whether to re-run partner_mail_start_connect.
partner_mail_set_modeSet the partner-wide mail provider. Switching to "microsoft" before completing partner_mail_start_connect errors 412 NO_CONNECTION (no refresh token on file). "off" is the recommended kill-switch — flipping it back to "resend" or "microsoft" resumes sending immediately.
partner_mail_disconnectRevoke the cached Microsoft refresh token and revert the provider to Resend. Use when the connected mailbox is compromised or the partner admin wants to re-consent under a different account. After disconnect, partner_mail_start_connect must be run again before partner_mail_set_mode can flip back to "microsoft".
partner_mail_send_testSend a one-off test email via Microsoft Graph using the connected mailbox (or sharedMailbox if set). Recipient is always the calling user's own email — no body to specify. Use to confirm the OAuth flow + Graph permissions are wired correctly. Errors 412 NOT_CONNECTED if the partner hasn't connected a Microsoft mailbox yet, or 502 GRAPH_<status> if Graph rejects the send (typically a missing Send-As permission on the shared mailbox).
partner_mail_set_shared_mailboxSet or clear the shared-mailbox UPN that customer-facing emails are sent FROM. When unset (null), mail is sent from the connected user's own mailbox. Confirm permission is wired correctly afterwards via partner_mail_send_test — Graph errors 502 if Send-As permission is missing on the shared mailbox.
partner_mail_start_connectReturns the Microsoft authorize URL for the partner admin to navigate to. The OAuth callback handles token persistence on the platform; no further MCP calls are needed for the consent step itself. After the callback bounces back to returnTo with ?mailConnected=1, call partner_mail_set_mode with mode="microsoft" to switch the provider on. Errors 503 MAIL_APP_NOT_CONFIGURED if the platform's Partner Mail Sender Entra app isn't configured server-side.

API Keys (3)

ToolWhat it does
partner_list_keysList the active API key slots (1 and 2) for the partner. Each slot exposes its number + a 4-char suffix of the raw key for visual identification — the raw key itself is never re-fetchable. Returned as part of the partner profile (the same shape partner_get_plan_billing already exposes). Use to confirm which slot is in use before partner_create_key (max 2) or partner_revoke_key.
partner_create_keyWARNING — raw key returned ONCE in response.key. Store immediately (e.g. in a secrets manager); it cannot be re-fetched. Costs an active slot (max 2). Throws MAX_KEYS if both slots are already in use — call partner_revoke_key on the unused slot first. Recommended rotation flow: create slot 2, deploy with new key, verify, then revoke slot 1.
partner_revoke_keyWARNING — irreversible. Revoke the API key in the given slot (1 or 2). The key is invalidated immediately; any caller still using it gets 401 on the next request. Foot- gun: revoking the slot the SDK / MCP server is currently using locks subsequent calls out of the platform. Always rotate (partner_create_key) and verify the new key works before revoking the old one.

Pipeline / Leads (10)

Requires the partner record to have prospecting in enabledFeatures (admin-granted only — no plan tier auto-includes it).

ToolWhat it does
partner_list_leadsList partner prospecting leads with optional stage / search filters and stage-count rollup. Requires the partner record to have prospecting in enabledFeatures (admin-granted only — no plan tier auto-includes it). Returns leads with domain, company, contact info, stage, and timestamps. Use when the user asks "show me my pipeline", "leads in stage X", or wants to look up a leadId.
partner_get_leadFetch one lead with its full contact list, partner overrides, lifecycle timestamps, and the most recent 50 activity log entries. Requires prospecting feature flag. Use when the user wants details on a specific lead (by id) or to inspect activity / enrichment results.
partner_create_leadCreate a single lead from a domain. Requires prospecting feature flag. By default runs enrichment synchronously: Apollo + Hunter populate company/contact fields, and an OIDC probe against login.microsoftonline.com fills tenantId/isOnM365 when the domain is on Entra. Returns 409 with existingLeadId if a lead for the same domain already exists for this partner.
partner_bulk_create_leadsBulk-create up to 50 leads from a list of domains. Requires prospecting feature flag. Returns created (lead rows) + skipped (duplicates / invalid domains with reasons). Enrichment runs in the background — partner_get_lead will return enriched data once Apollo / Hunter / M365 finish (typically <30s per lead).
partner_update_leadUpdate whitelisted lead fields. Requires prospecting feature flag. Stage cannot be set via this tool — use partner_set_lead_stage for terminal Won/Lost transitions; other stages are server-managed by the assessment lifecycle. Identity links (customerId, assessmentId) are also server-managed and not partner-settable.
partner_delete_leadSoft-delete a lead. Requires prospecting feature flag. Cancels any in-flight outreach cadence. The row stays in Postgres for audit; re-creating the same domain after deletion creates a fresh lead row with a new id. Idempotent — re-deleting an already-deleted lead returns 404.
partner_enrich_leadRe-run enrichment for a lead: Apollo people search + Hunter domain search + Microsoft 365 OIDC tenant discovery (login.microsoftonline.com/{domain}/.well-known/openid-configuration). Requires prospecting feature flag. Returns the updated lead, the contributing sources (apollo, hunter, m365), and fromCache: true when results were served from the freshness cache. On successful enrichment the lead auto-advances from New to Enriched.
partner_set_lead_stageMark a lead Won or Lost — the two terminal states. Requires prospecting feature flag. Other stages (Enriched / OutreachSent / Consented / ReportDelivered) are server-managed via the assessment lifecycle and cannot be set explicitly. Setting Won or Lost cancels any in-flight outreach cadence for the lead. WON SIDE-EFFECTS: fires the lead.won webhook event. If the partner has the lead-deferred-customer feature flag AND the lead has no customer attached yet, also materialises a partner_customer row from the lead's primary contact + tenant, backfills any orphan partner_runs rows with the new customerId, and fires customer.created once. Idempotent — re-Winning a lead that already has a customer is a no-op (no second customer, no duplicate events).
partner_list_lead_activityRead the activity log for a lead — created, enriched, email_sent, email_delivered, email_opened, email_clicked, consented, report_ready, stage_changed, note, etc. Each entry has type / detail / metadata / actorEmail / createdAt. Requires prospecting feature flag. Use when the user asks "what happened to this lead" or to debug an outreach sequence.
partner_send_lead_consentKick off the partner-branded outreach cadence for a lead. Mints an assessment + run row (COSTS 1 CREDIT), sends touch-1 of the consent email to each recipient, advances the lead to OutreachSent, and fires the lead.outreach_sent webhook event. Touches 2 + 3 fire from the background cadence worker on day 3 + 7. Suppressed addresses are skipped silently with skipped: "suppressed" in the per-recipient result. CUSTOMER CREATION: depends on the partner's lead-deferred-customer feature flag. OFF (default): a partner_customer row is created (or attached by email match) at this call and returned as customerId. ON (#369): no customer is created here — customerId in the response is null, the run row carries lead_id, and a customer is materialised only when the partner later marks the lead Won via partner_set_lead_stage. Dispatch is routed by the partner's mail mode (mailProvider): resend (default — baref00t Resend with partner From-name + Reply-To), microsoft (partner's connected Graph mailbox), or off (call fails with MAIL_DISABLED unless skipEmail: true). Each per-recipient result reports via: "resend" | "microsoft" so the caller can confirm which channel ran. Pass skipEmail: true to bypass the email send entirely — the response still carries the per-recipient consentUrl strings so the partner can dispatch via their own channel. Requires the prospecting feature flag. Reject if the lead is in a terminal stage (Won / Lost).

Distributor (2)

ToolWhat it does
distributor_list_partnersList all sub-partners owned by this distributor. Optionally filter by status. Returns each partner's id, company, email, plan, billing, and status.
distributor_get_usageAggregated assessment usage across all sub-partners for a billing month. Returns totalPartners, activePartners, totalRuns, and per-partner breakdown (runs vs limit). Use to answer "how much have my sub-partners used this month?".

PII: customer lists, lead contact lists, and recipient breakdowns include email addresses (and other contact data from Apollo / Hunter enrichment) which flow to the LLM provider's pipeline as tool inputs / outputs. baref00t doesn't control how the AI provider stores those interactions. Confirm your contractual basis (e.g. Anthropic Zero Data Retention, OpenAI no-train terms) with your AI provider before connecting a partner key that has access to real customer data — emails are returned plaintext, not hashed.

Self-host the partner portal

White-label, open-source partner portal you can run on your own infrastructure with your own Microsoft Entra SSO, your own domain, and your own outbound mail sender. Built on Next.js, uses @baref00t/sdk against the Partner API.

Source: github.com/becloudsmart-com/baref00t-portal — Apache-2.0.

Run with Docker

docker pull ghcr.io/becloudsmart-com/baref00t-portal:latest

docker run -p 3000:3000 \
  -e BAREF00T_API_KEY=pk_live_... \
  -e AUTH_SECRET=$(openssl rand -base64 32) \
  -e AUTH_URL=https://portal.acme.com \
  -e AZURE_AD_TENANT_ID=<your-entra-tenant-guid> \
  -e AZURE_AD_CLIENT_ID=<your-app-reg-guid> \
  -e AZURE_AD_CLIENT_SECRET=<your-app-secret> \
  -e BRAND_NAME="Acme Cloud Security" \
  -e BRAND_PRIMARY_COLOR="#00cc66" \
  -e BRAND_LOGO_URL="https://cdn.acme.com/logo.svg" \
  -e MAIL_FROM_ADDRESS="security-no-reply@acme.com" \
  ghcr.io/becloudsmart-com/baref00t-portal:latest

What's included

Customer management (CRUD + bulk import)
Assessment trigger wizard + report viewer (uses /assessments/:id/report)
Webhook configuration (uses /webhooks CRUD)
API key rotation UI
Plan + usage display (read-only — billing happens on the hosted dashboard)
Branding from env (logo / primary color / footer / favicon)
Outbound mail via your Microsoft Graph mailbox

See docs/AZURE-SETUP.md, docs/MAIL-SETUP.md, and docs/ENV-REFERENCE.md in the public repo for step-by-step setup.