GuidesAPI ReferenceChangelog
Log In

Technical Implementation

Step 1: Subscribe to Webhook Events

Pinwheel executes jobs in the background, so the best way to be notified when data is available is to subscribe to webhooks. As a user's account is connected and data is collected, events are posted to your webhook to alert your app that the data is ready for consumption. Webhook events can be customized to your use case, so subscribe to the jobs your application requires. The account.added webhook signals a user's account has been connected, and the rest of the webhooks are job specific.

POST /v1/webhooks
Host: api.getpinwheel.com
Content-Type: application/json
x-api-secret: YOUR-API-SECRET
{
  "url": "https://your-domain.com/webhook_endpoint",
  "status": "ACTIVE",
  "enabled_events": [
    "account.added",
    "employment.added",
    "income.added",
    "paystubs.added",
    "shifts.added",
  ]
}

The complete guide to webhooks can be found here.

Step 2: Create a Link token

Create a Link token specifying the jobs that your application requires using the required_jobs parameter.

POST /v1/link_tokens
Host: api.getpinwheel.com
Content-Type: application/json
x-api-secret: YOUR-API-SECRET
{
  "org_name": "YOUR APP NAME",
  "end_user_id": "my_user_12345",
  "required_jobs": ["employment", "income", "paystubs"]
}

Note: The end_user_id is your internal reference to the end user. See User Model for more information.

The response includes a short-lived token that expires after 15 minutes and a unique id. Persist the id in your database.

{
  "data": {
    "id": "97f420ff-5d0a-46ee-9cfc-6f17d5d31256",
    "expires": "2021-01-09T02:52:26+00:00",
    "mode": "production",
    "token": "eyJ0eXAiOiJKV1QiLCJhbGci...cyldX8fILelb6A0XKmdWsXZHMH4W1o",
  }
}

Step 3: Initialize Link

Using the Link token that was created, open the Link modal in your client application. In addition to passing in the token, you can optionally pass in several callback handlers.

<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdn.getpinwheel.com/pinwheel-v2.3.js"></script>
    <script>
      window.addEventListener("load", () => {
        Pinwheel.open({
          linkToken: "INSERT LINK TOKEN",
        });
      });
    </script>
  </head>
  <body></body>
</html>

Calling Pinwheel.open() renders the Link modal in full screen mode.

Step 4: Responding to Webhook Events

The account.added webhook event is published after a user logs in to their payroll account. Using the link_token_id you persisted earlier, you can associate the account with the user who logged in with Link.

{
  "event": "account.added",
  "event_id": "5a141122-4235-4fa1-bd76-0628573880b0",
  "payload": {
    "account_id": "03bbc20e-bc39-464a-b4dc-4b63ffb7213d",
    "end_user_id": "my_user_12345",
    "link_token_id": "97f420ff-5d0a-46ee-9cfc-6f17d5d31256",
    "platform_id": "fce3eee0-285b-496f-9b36-30e976194736",
    "created_at": "2021-01-12T02:36:01.287148+00:00",
    "connected": true
  }
}

Subsequent webhook events are published when data for a job is available. For example, the employment.added event is sent once employment data is ready.

{
  "event": "employment.added",
  "event_id": "5a141122-4235-4fa1-bd76-0628573880b0",
  "payload": {
    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
    "account_id": "03bbc20e-bc39-464a-b4dc-4b63ffb7213d",
    "end_user_id": "my_user_12345",
    "link_token_id": "7c4ac4be-4a0e-4468-ab26-c42b249b233b",
    "name": "employment",
    "timestamp": "2021-01-21T20:16:28+00:00",
    "outcome": "success",
    "error_code": null
  }
}
{
  "event": "paystubs.added",
  "event_id": "5a141122-4235-4fa1-bd76-0628573880b0",
  "payload": {
    "id": "24fe697f-a893-42a5-adca-a727f11ad792",
    "account_id": "03bbc20e-bc39-464a-b4dc-4b63ffb7213d",
    "end_user_id": "my_user_12345",
    "link_token_id": "7c4ac4be-4a0e-4468-ab26-c42b249b233b",
    "name": "paystubs",
    "timestamp": "2021-01-21T20:16:28+00:00",
    "outcome": "success",
    "error_code": null,
    "added": [{"id": "dbff9830-55ce-4aa5-82f1-e04f196da041"}],
    "deleted": [{"id": "57043868-6fb3-47e8-a2b7-c993bd023f25"}],
    "params": {
      "from_pay_date": "2020-10-01",
      "to_pay_date": "2020-12-31",
      "count": 6,
      "sync_status": "in_progress",
    }
  }
}
{
  "event": "income.added",
  "event_id": "5a141122-4235-4fa1-bd76-0628573880b0",
  "payload":{
    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
    "account_id": "449e7a5c-69d3-4b8a-aaaf-5c9b713ebc65",
    "end_user_id": "my_user_12345",
    "link_token_id": "4787acbc-11cf-4db3-998c-5ea7c4feebcd",
    "name": "income",
    "timestamp": "2021-01-12T02:36:01.287148+00:00",
    "outcome": "success"
  }
}
{
  "event": "identity.added",
  "event_id": "5a141122-4235-4fa1-bd76-0628573880b0",
  "payload":{
    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
    "account_id": "449e7a5c-69d3-4b8a-aaaf-5c9b713ebc65",
    "end_user_id": "my_user_12345",
    "link_token_id": "4787acbc-11cf-4db3-998c-5ea7c4feebcd",
    "name": "identity",
    "timestamp": "2021-01-12T02:36:01.287148+00:00",
    "outcome": "success"
  }
}
{
  "event": "shifts.added",
  "event_id": "5a141122-4235-4fa1-bd76-0628573880b0",
  "payload":{
    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
    "account_id": "449e7a5c-69d3-4b8a-aaaf-5c9b713ebc65",
    "end_user_id": "my_user_12345",
    "link_token_id": "4787acbc-11cf-4db3-998c-5ea7c4feebcd",
    "name": "shifts",
    "timestamp": "2021-01-12T02:36:01.287148+00:00",
    "outcome": "success",
    "added": [{"id": "dbff9830-55ce-4aa5-82f1-e04f196da041"}],
    "deleted": [{"id": "57043868-6fb3-47e8-a2b7-c993bd023f25"}],
    "params": {
      "count": 14,
      "sync_status": "complete",
    }
  }
}

For accounts with a lot of data, gathering every available shift and paystub can take quite some time. To communicate this progress, both the paystubs.added and shifts.added webhook events have a payload.parms.sync_status field letting you know when Pinwheel is done syncing data from your user's payroll platform. A status of in_progress means we are still collecting data, while a status of complete mean we are done.

If you would instead prefer to be notified after specific days of data have been synced, you can subscribe to any of the sync status events which fire as soon as the relevant data has been retrieved. The sync status events are:

  • paystubs.seven_days_synced: Triggered when 7 days of paystubs have been collected
  • paystubs.thirty_days_synced: Triggered when 30 days of paystubs have been collected
  • paystubs.ninety_days_synced: Triggered when 90 days of paystubs have been collected
  • paystubs.fully_synced: Triggered when all available paystubs have been collected

You can use these sync events to fit your use case. E.g, if you only require a single paystub, you can subscribe to paystubs.seven_days_synced. This event will trigger once 7 days has elapsed from the first observed paystub, i.e. you will be notified as soon as the first paystub is available. Similarly, you would subscribe to the paystubs.thirty_days_synced event if you need at least 2 paystubs.

For Shifts, follow the same convention but swap out the prefix, e.g., shifts.seven_days_synced.

{
  "event": "paystubs.seven_days_synced",
  "event_id": "5a141122-4235-4fa1-bd76-0628573880b0",
  "payload":{
    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
    "account_id": "449e7a5c-69d3-4b8a-aaaf-5c9b713ebc65",
    "end_user_id": "my_user_12345",
    "link_token_id": "4787acbc-11cf-4db3-998c-5ea7c4feebcd",
    "name": "paystubs",
    "timestamp": "2021-01-12T02:36:01.287148+00:00",
    "outcome": "success",
    "params": {
      "from_pay_date": "2020-12-01",
      "to_pay_date": "2020-12-31",
      "count": 1
    }
  }
}
{
  "event": "shifts.seven_days_synced",
  "event_id": "5a141122-4235-4fa1-bd76-0628573880b0",
  "payload":{
    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
    "account_id": "449e7a5c-69d3-4b8a-aaaf-5c9b713ebc65",
    "end_user_id": "my_user_12345",
    "link_token_id": "4787acbc-11cf-4db3-998c-5ea7c4feebcd",
    "name": "shifts",
    "timestamp": "2021-01-12T02:36:01.287148+00:00",
    "outcome": "success",
    "params": {
      "count": 6
    }
  }
}

Occasionally there might be a delay until the data is available. In these cases, we send a webhook with a pending outcome. This indicates that a job was unable to be completed on its first attempt and will be retried up to 24 hours after the initial attempt. A job with a pending outcome will transition to either an error or a success within the 24 hour window and you will be notified via webhook.

Step 5: Retrieving Income and Employment Data

Upon receiving the relevant webhook events, you will make API requests to retrieve data from the different Income and Employment endpoints.

Employment

Request

GET /v1/accounts/03bbc20e-bc39-464a-b4dc-4b63ffb7213d/employment
Host: api.getpinwheel.com
Content-Type: application/json
x-api-secret: YOUR-API-SECRET

Response

See the full Employment API reference here.

{
  "data": {
    "id": "077f2bf2-acfe-44b6-9751-efafc5f97341",
    "account_id": "03bbc20e-bc39-464a-b4dc-4b63ffb7213d",
    "status": "employed",
    "title": "Sales Associate",
    "start_date": "2019-03-27",
    "employer_name": "Walmart",
    "created_at": "2021-01-09T21:21:25.256571+00:00"
  }
}

Paystubs

Request

GET /v1/accounts/03bbc20e-bc39-464a-b4dc-4b63ffb7213d/paystubs?limit=1
Host: api.getpinwheel.com
Content-Type: application/json
x-api-secret: YOUR-API-SECRET

Response

See the full Paystubs API reference here.

{
  "meta": {
    "count": 1
  },
  "data": [
    {
      "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
      "account_id": "03bbc20e-bc39-464a-b4dc-4b63ffb7213d",
      "pay_date": "2020-12-31",
      "pay_period_start": "2020-12-10",
      "pay_period_end": "2020-12-24",
      "currency": "usd",
      "gross_pay_amount": 480769,
      "net_pay_amount": 273563,
      "check_amount": 280563,
      "gross_pay_ytd": 6971151,
      "net_pay_ytd": 4357992,
      "total_taxes": 139680,
      "total_deductions": 66704,
      "total_reimbursements": 7000,
      "taxes": [
        {
          "name": "Federal Income Tax",
          "category": "federal_income",
          "amount": 65158
        },
        {
          "name": "Social Security",
          "category": "social_security",
          "amount": 29249
        },
        {
          "name": "Medicare",
          "category": "medicare",
          "amount": 6840
        },
        {
          "name": "New York Income Tax",
          "category": "state",
          "amount": 22860
        },
        {
          "name": "New York City Income Tax (New York County)",
          "category": "local",
          "amount": 15573
        }
      ],
      "deductions": [
        {
          "name": "Aetna PPO",
          "category": "medical_insurance",
          "amount": 7012,
          "type": "pre_tax"
        },
        {
          "name": "401(k) deferral",
          "category": "retirement",
          "amount": 57692,
          "type": "pre_tax"
        },
        {
          "name": "Transit",
          "category": "commuter",
          "amount": 2000,
          "type": "pre_tax"
        }
      ],
      "earnings": [
        {
          "name": "Regular Earnings",
          "category": "salary",
          "amount": 480769,
          "rate": null,
          "hours": null
        }
      ],
      "created_at": "2021-01-10T19:54:45.745660+00:00"
    }
  ]
}

Income

Request

GET /v1/accounts/03bbc20e-bc39-464a-b4dc-4b63ffb7213d/income
Host: api.getpinwheel.com
Content-Type: application/json
x-api-secret: YOUR-API-SECRET

Response

See the full Income API reference here.

{
  "data": {
    "id": "6309e185-9c87-4384-aaae-767c7875775e",
    "account_id": "eb240ec6-a227-47ca-b7c7-fbbdfe7170a0",
    "created_at": "2021-01-06T15:59:13.530178+00:00",
    "compensation_amount": 400000,
    "compensation_unit": "monthly",
    "currency": "USD"
  }
}

Identity

Request

GET /v1/accounts/03bbc20e-bc39-464a-b4dc-4b63ffb7213d/identity
Host: api.getpinwheel.com
Content-Type: application/json
x-api-secret: YOUR-API-SECRET

Response

See the full Identity API reference here.

{
  "data": {
    "id": "ffec8d06-5d03-42b9-9f75-6c66cb6efed5",
    "account_id": "eb240ec6-a227-47ca-b7c7-fbbdfe7170a0",
    "created_at": "2021-01-06T15:59:13.530178+00:00",
    "full_name": "Alicia Green",
    "date_of_birth": "1987-02-01",
    "last_four_ssn": "0090",
    "emails": [
      "[email protected]"
    ],
    "address": {
      "raw": "1234 Home Road, Washington, DC, 20017, USA",
      "line1": "1234 Home Road",
      "line2": null,
      "city": "Washington",
      "state": "DC",
      "postal_code": "20017",
      "country": "US"
    },
    "phone_numbers": [
      {
        "value": "+14155552671",
        "type": "mobile"
      },
      {
        "value": "+14155551234",
        "type": "home"
      }
    ]
  }
}

Shifts

Request

GET /v1/accounts/03bbc20e-bc39-464a-b4dc-4b63ffb7213d/shifts
Host: api.getpinwheel.com
Content-Type: application/json
x-api-secret: YOUR-API-SECRET

Response

See the full Shifts API reference here.

{
  "data": [
    {
      "id": "c134b9bd-29f8-452b-bfaa-0dbcc6f6f026",
      "account_id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
      "created_at": "2021-01-29 22:06:06.920124+00:00",
      "start_date": "2021-01-26",
      "end_date": "2021-01-26",
      "type": "shift",
      "timezone": "America/New_York",
      "timestamps": [
        {
          "from": "2021-01-26T12:00:00.745660+00:00",
          "to": "2021-01-26T20:21:00.745660+00:00"
        }
      ],
      "earnings": [
        {
          "name": "Regular Hours",
          "category": "hourly",
          "hours": 8,
          "rate": 2875,
          "amount": 23000
        },
        {
          "name": "Overtime",
          "category": "overtime",
          "hours": 0.35,
          "rate": 4311,
          "amount": 1509
        }
      ],
      "currency": "USD"
    }
  ],
  "meta": {
    "count": 1
  }
}

Step 6 (Optional): Query for Job Results

If your application never subscribed to webhook events, or your application server failed to handle the webhook events, you can query the Jobs endpoint with the ID of the Link token used to initialize Link to fetch the results.

Request

GET /v1/jobs?link_token_id=97f420ff-5d0a-46ee-9cfc-6f17d5d31256&job_types=employment
Host: api.getpinwheel.com
Content-Type: application/json
x-api-secret: YOUR-API-SECRET

Response

{
  "meta": {
    "count": 1
  },
  "data": [
    {
      "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
      "account_id": "03bbc20e-bc39-464a-b4dc-4b63ffb7213d",
      "link_token_id": "7c4ac4be-4a0e-4468-ab26-c42b249b233b",
      "name": "employment",
      "timestamp": "2021-01-12T02:36:01.287148+00:00",
      "outcome": "success",
      "params": {
        "amount": null
      }
    }
  ]
}

Please contact [email protected] for access to our Dashboard.