How to Push Approved Timesheets to Your PSA's REST API and Log to MySQL

Build a weekly Spojit workflow that pulls approved timesheets from Deputy, signs each payload, posts every entry to your professional-services automation (PSA) tool's REST API, and writes a billable-hours ledger row to MySQL.

What This Integration Does

Professional-services teams capture hours in a workforce app like Deputy but bill through a separate PSA or practice-management system. Re-keying approved time by hand each week is slow and error-prone. This workflow closes the gap: it reads the approved timesheets straight from Deputy, sends each one to your PSA over its REST API, and keeps a parallel ledger in MySQL so you have a reportable record of every hour that was pushed and when. Because your PSA has no native tile in Spojit, you reach it honestly through the http connector, which can call any REST endpoint.

The workflow runs on a Schedule trigger, typically once a week after timesheets are approved. On each run it lists the current approved timesheets, then loops over them: a code step signs the outgoing payload, an http step posts it to your PSA, and a MySQL insert-rows step records a ledger line. State left behind is a row per pushed entry in your billable_hours table. Re-runs are safe as long as you guard against duplicates using a unique key in MySQL (covered in Pitfalls), since the same week can otherwise be posted twice.

Prerequisites

  • A Deputy connection in Spojit (added under Connections -> Add connection) with permission to read timesheets.
  • A MySQL connection pointing at the database that holds your billable-hours ledger.
  • A billable_hours table in MySQL with columns such as timesheet_id, employee_id, work_date, hours, psa_status, and pushed_at, plus a unique key on timesheet_id.
  • Your PSA's REST API base URL, the endpoint that accepts a time entry, an API key or token, and the signing scheme it expects (for example an HMAC header). The http connector is built in and needs no connection.
  • A way to know which timesheets are approved. Deputy timesheets carry status fields you can filter on (see Step 2).

Step 1: Add a Schedule trigger

Open the Workflow Designer and add a Trigger node, set its type to Schedule. Schedules use a 5-field Unix cron expression and an IANA timezone. To run every Monday at 9am Sydney time, use the expression 0 9 * * 1 with timezone Australia/Sydney. A single trigger can hold several schedules if you want more than one run window. The trigger output is { scheduledAt }, which you can reference later as {{ trigger.scheduledAt }} to stamp the ledger.

Step 2: List approved timesheets from Deputy

Add a Connector node on the Deputy connector in Direct mode and choose the list-timesheets tool. Direct mode is the right choice here: it is deterministic, calls exactly one tool, and costs no AI credits. The tool returns all timesheets in a results envelope; reference it downstream as {{ timesheets.data }} (use the output-variable name you give the node).

Deputy timesheets include status fields that indicate whether an entry has been approved. Add a Transform node after the list step to keep only approved, billable rows and to shape each one into the exact fields your PSA and ledger need. A trimmed entry might look like:

{
  "timesheetId": 48213,
  "employeeId": 1042,
  "workDate": "2026-06-15",
  "hours": 7.5,
  "project": "ACME-RETAINER"
}

If you prefer not to hand-write the filter, ask Miraxa, the intelligent layer across your automation, to "add a Transform node that keeps only approved Deputy timesheets and outputs id, employeeId, workDate and hours". You can then fine-tune the mapping in the properties panel.

Step 3: Loop over each timesheet

Add a Loop node in ForEach mode and point it at the filtered list, for example {{ approved.entries }}. Everything inside the loop body runs once per timesheet, and the current item is available as the loop item variable (for example {{ entry }}). Keeping the sign-post-log sequence inside the loop means each entry is processed independently, so one bad row does not block the rest when you combine this with error handling.

Step 4: Sign the payload with the code connector

Inside the loop body, add a Connector node on the code connector in Direct mode and choose execute-javascript. Use it to build the JSON body your PSA expects and to compute the signature header (for example an HMAC of the body using your shared secret). Returning both the body and the signature from this step keeps the next node clean.

const body = {
  external_id: String(input.entry.timesheetId),
  employee: input.entry.employeeId,
  date: input.entry.workDate,
  hours: input.entry.hours,
  project_code: input.entry.project
};
const payload = JSON.stringify(body);
const signature = hmacSha256(payload, env.PSA_SIGNING_SECRET);
return { payload, signature, body };

Keep your signing secret out of the script body by passing it in as configured input rather than hard-coding it. If your PSA does not require signing, you can skip straight to Step 5 and post the body directly.

Step 5: Post each entry to your PSA's REST API

Still inside the loop, add a Connector node on the http connector in Direct mode and choose http-post. This is how Spojit reaches a system that has no native tile: you call the REST endpoint directly. Set the fields:

  • url: your PSA time-entry endpoint, for example https://api.yourpsa.com/v1/time-entries.
  • body: the JSON body produced in Step 4, for example {{ sign.body }}.
  • headers: your auth and signature headers, for example Authorization: Bearer YOUR_TOKEN and X-Signature: {{ sign.signature }}.

The node returns the response status and body so you can branch on success. For a deeper walkthrough of calling external services this way, see the Spojit tutorial on connecting to any REST API.

Step 6: Write a ledger row to MySQL

After the POST, add a Connector node on the MySQL connector in Direct mode and choose insert-rows. Set table to billable_hours and pass a single-element array in rows built from the current entry, the PSA response, and the trigger time:

{
  "table": "billable_hours",
  "rows": [
    {
      "timesheet_id": "{{ entry.timesheetId }}",
      "employee_id": "{{ entry.employeeId }}",
      "work_date": "{{ entry.workDate }}",
      "hours": "{{ entry.hours }}",
      "psa_status": "{{ post.status }}",
      "pushed_at": "{{ trigger.scheduledAt }}"
    }
  ]
}

Optionally add a Condition node before the insert so you only log a success row when the PSA returned a 2xx status, and route failures to a Send Email node that alerts your operations inbox.

Tips

  • Filter to approved timesheets as early as possible (Step 2) so you never post draft or rejected time to billing.
  • Use Deputy get-timesheet if you need the full detail of a single entry that the list view trims, for example to fetch a comment or cost field.
  • Batch the MySQL write if volume is high: insert-rows accepts multiple objects in rows, so you can collect entries and insert once at the end instead of once per loop iteration.
  • Stamp every ledger row with {{ trigger.scheduledAt }} so each weekly run is auditable and easy to query later.

Common Pitfalls

  • Duplicate pushes on re-run. Put a unique key on timesheet_id in MySQL so a re-run of the same week fails the insert instead of double-billing. You can also check for an existing row with an execute-query step before posting.
  • Timezone drift. The Schedule trigger fires in the IANA zone you set, but Deputy work dates may be in another zone. Decide one canonical zone and convert in the Transform step to avoid hours landing on the wrong day.
  • Signature mismatches. If your PSA rejects requests, confirm you are signing the exact bytes you send. Build the body string once in the code step and reuse it for both the signature and the http-post body.
  • Silent PSA errors. A non-2xx response from http-post does not stop the loop by default. Add a Condition node on the response status so failed entries are caught and reported, not logged as successes.

Testing

Before scheduling, validate on a tiny scope. Temporarily filter the Transform step to a single known timesheet, point http-post at your PSA's sandbox endpoint, and run the workflow with the Run button. Check the execution logs to confirm the Deputy list returned the entry, the code step produced a valid signature, the POST returned a 2xx status, and exactly one row appeared in billable_hours. Run it a second time to confirm your unique key blocks the duplicate. Once a single entry round-trips cleanly, widen the filter and enable the schedule.

Learn More

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.