PSA Integrations & Billing

Overview

Integrations → PSA (/integrations/psa) holds two connectors that turn approved time from Time Tracking & Timesheets into your billing workflow:

  • Generic signed webhook: pushes time and billing events to any endpoint you control. Every delivery is signed with HMAC-SHA256, so it works with any PSA middleware, Zapier, or Make.
  • Stripe billing connector: generates draft Stripe invoices from approved billable time, one connector per client organization.

Both are configured per organization and managed by admins. Generating invoice drafts additionally requires the billing_approver privilege (granted by a workspace owner; see the roles section).

Whose Stripe account? Yours. The connector uses a restricted API key from your Stripe account, drafts land in your account, and payments (once you finalize and send) flow directly to you. This is entirely separate from the Stripe system ET Ducky uses for its own subscription billing: no shared account, no Stripe Connect relationship, and ET Ducky never sees or touches your customers' payments.

Set up the signed webhook

  1. Go to Integrations → PSA
  2. Enter your endpoint URL. It must be https
  3. Select the event subscriptions you want delivered (see the event reference below)
  4. Save. The signing secret is generated on the first save and shown once. Copy it into your receiver's configuration now; it is not displayed again
  5. Turn on the enable toggle
  6. Click Test to send a test delivery and confirm your endpoint accepts it

If you lose the secret, rotate it from the same card. Rotation generates a new secret (again shown once) and invalidates the old one, so update your receiver at the same time.

Verifying deliveries

Every delivery carries two headers:

  • X-ETDucky-Event: the event name, for example timesheet.approved
  • X-ETDucky-Signature: sha256=<hex>, the hex HMAC-SHA256 of the raw request body, keyed with your webhook secret

The body has the shape { event, organizationId, idempotencyKey, sentAt, data }. Use idempotencyKey to deduplicate retried deliveries. Verify the signature against the raw body bytes, before any JSON parsing. In Node:

const crypto = require('crypto');

function verify(rawBody, signatureHeader, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody) // raw request body, not re-serialized JSON
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}

Event reference

EventSent when
time_entry.capturedAuto-capture creates a draft time entry, forwarded by the Push Time Entry to PSA automation action
timesheet.submittedA member submits a Monday-Sunday week for approval
timesheet.approvedA timesheet approver approves a submitted week
time_entry.approvedAn individual time entry is approved (as part of week approval), with its snapshotted hourly rate
billing.invoice_draftedA draft Stripe invoice is created for the organization

You only receive the events you subscribed to on the webhook card.

Connect ET Ducky to your PSA

ET Ducky has no native PSA connector — it emits the signed webhook events above, and you route them into your PSA. Two paths:

  • An iPaaS bridge (Zapier or Make) — easiest. Point ET Ducky's webhook at a catch-hook, verify the signature, then call your PSA's “create time entry” step. Nothing to host.
  • Your PSA's own inbound webhook / REST API. Receive the event on a small endpoint, verify the signature, and create the time entry via the PSA's API.

The event that matters for time sync is time_entry.approved: its entries array holds approved time, each entry already tagged with the fields a PSA time entry needs. (Subscribe to time_entry.captured instead if you want drafts pushed the moment auto-capture creates them, via the Push Time Entry to PSA automation action.)

Field mapping reference

From each element of data.entries in a time_entry.approved delivery:

ET Ducky fieldTypical PSA time-entry field
ticketIdThe ticket to attach the time to (service ticket number). Empty when the entry wasn't tied to a ticket.
durationSecondsTime worked — divide by 3600 for hours.
descriptionTime-entry notes / work summary.
categoryWork type / work role.
billingStatusBillable vs non-billable flag.
agentIdThe device the work was on — map to a PSA configuration item / asset if you track those.
rateCentsSnapshotThe approval-time rate (cents). PSAs usually derive rate from the work role; use this only to override.
id + envelope idempotencyKeyDedupe keys — skip if you've already created this entry.
envelope data.clerkUserIdThe member / resource — map the ET Ducky user to the PSA member once.

Exact PSA field and endpoint names vary by product and version; treat this as the source side of the mapping and confirm the target names in your PSA's API docs.

Recipe: Zapier or Make (recommended)

  1. Create a Catch Hook (Zapier) or Custom Webhook (Make) trigger and copy its URL.
  2. In ET Ducky (Integrations → PSA), set that URL as the endpoint, subscribe to time_entry.approved, save, copy the secret, enable, and click Test.
  3. Add a Code step that recomputes sha256=HMAC-SHA256(rawBody, secret) and compares it to the X-ETDucky-Signature header (the Node snippet under Verifying deliveries above); stop the run on a mismatch.
  4. Add a loop over data.entries; for each, call your PSA app's Create Time Entry action mapped per the table above. Use idempotencyKey to avoid duplicates on retries.

Zapier and Make ship native ConnectWise Manage, Autotask, and HaloPSA apps, so the final step is usually a prebuilt action rather than a raw API call.

Recipe: ConnectWise Manage

Create the entry with POST /v4_6_release/apis/3.0/time/entries (or the ConnectWise app in your iPaaS).

  • Auth: a dedicated API member with a public/private key pair; authenticate as companyId+publicKey : privateKey (Basic) with a clientId header.
  • Map: ticketIdchargeToId (chargeToType = ServiceTicket), durationSeconds/3600actualHours, descriptionnotes, category → work type/role, billingStatusbillableOption (Billable/DoNotBill), clerkUserIdmember.identifier.

Recipe: Autotask (Datto)

Create the entry with POST /ATServicesRest/V1.0/TimeEntries (or the Autotask app in your iPaaS).

  • Auth: an API-User security-level account; send UserName, Secret, and your ApiIntegrationCode headers.
  • Map: ticketIdticketID, durationSeconds/3600hoursWorked, descriptionsummaryNotes, categoryroleID / billingCodeID, billingStatusisNonBillable (invert), clerkUserIdresourceID.

Recipe: HaloPSA

Log time as an action on the ticket with POST /api/Actions (or the HaloPSA app in your iPaaS).

  • Auth: OAuth2 client-credentials — exchange a client id/secret for a bearer token, then call the API with it.
  • Map: ticketIdticket_id, durationSeconds/3600timetaken, descriptionnote, category → action type / charge rate, billingStatus → non-billable flag, clerkUserId → the agent / who field.

Set up Stripe billing

The Stripe connector is configured per client organization and is admin-managed. It needs two values from your Stripe account (the MSP's account): a restricted API key and the customer that represents this organization. Invoices are created in your account and payments flow to you; ET Ducky's own subscription billing is a separate system and plays no part in this.

  1. In the Stripe dashboard, create a restricted API key (rk_...) with exactly these scopes: Invoices: Write and Customers: Read
  2. Find (or create) the Stripe customer (cus_...) that represents this client organization in your Stripe account. The customer must exist before you connect; ET Ducky never creates customers
  3. In ET Ducky, go to Integrations → PSA and open the Stripe billing connector. Paste the restricted key and the Customer ID, and turn on the enable toggle
  4. Click Test connection. It retrieves the customer's name from Stripe so you can confirm you connected the right customer
  5. Have a workspace owner grant the billing_approver privilege (Configuration → Time → Roles & privileges) to whoever will generate invoices. It is not implied by admin or owner
  6. Generate a draft: click Generate invoice draft on the time report card, or set up the Create Invoice Draft (Stripe) automation action on a Scheduled trigger for recurring billing
  7. Review and finalize the draft in the Stripe dashboard. ET Ducky never finalizes, sends, or charges anything

The API key is stored encrypted and never displayed again. On later saves, leave the key field blank to keep the stored key; only enter a value to replace it.

How invoice drafts are built

  • Each generation creates one draft invoice for the billing period: the previous week (Monday-Sunday, the default) or the previous calendar month, computed at run time
  • Line items are hours × rate per category, built from approved billable time. Rates come from the approval-time snapshots, so rate changes after approval never change the invoice (see rate snapshots)
  • Deterministic idempotency keys prevent double-drafting: generating twice for the same organization and period does not create a second invoice
  • Nothing is ever finalized, sent, or charged by ET Ducky. Drafts wait in Stripe for your review

The per-client-org model for MSPs

If you run one ET Ducky organization per client, the billing model maps onto that directly:

  • One Stripe account (yours), used by every connector via the same or separate restricted keys
  • One Stripe customer per managed organization: each client org's connector points at the customer that represents that client in your Stripe account
  • Rates and roles are per organization: the same technician can bill $150/hour in one client org and $175/hour in another, and privileges like timesheet_approver and billing_approver must be granted in each org where they are needed

Generating drafts org by org therefore produces one correctly-addressed draft invoice per client, each priced with that client's rates.

Limits

  • Your Stripe account, not ours. Draft invoices are created in your organization's own Stripe account using the restricted API key you provide, and payments (once you finalize and send) flow directly to you. ET Ducky's own subscription billing is a separate system that never touches your key, your customers, or your invoices, and no Stripe Connect relationship exists between the two
  • Drafts only. ET Ducky never finalizes, sends, or charges an invoice; do that in the Stripe dashboard
  • One invoice per organization per period. Idempotency keys make repeat generations for the same period a no-op
  • The Stripe customer must pre-exist. The connector does not create customers
  • Webhook endpoints must be https, and the secret is shown only once per generation or rotation

Related

See Time Tracking & Timesheets for entries, weekly submission, approval, rates, and reports, and Automations for the Push Time Entry to PSA and Create Invoice Draft (Stripe) actions and the Time Entry Captured and Timesheet Submitted triggers.