Gareth Wilson Gareth Wilson

Guide to Argyle Webhooks: Features and Best Practices

Published


Argyle connects to a person's payroll or gig platform account, with their consent, and returns their employment, income and pay data to lenders, landlords and fintech apps. Almost everything Argyle does happens after the user has left your flow: an account finishes connecting, years of paystubs sync in the background, a W-2 is scanned, a verification report becomes ready. Webhooks are how your backend learns that each of those things has happened, so you can fetch the data or move a loan file forward without polling.

This guide covers how Argyle subscriptions are scoped, the event families and their sync events, how to sign deliveries with a secret you choose, and the single retry that makes an Argyle receiver less forgiving than most.

What are Argyle webhooks?

An Argyle webhook subscription has a name, an HTTPS URL, a list of events, an optional secret and an optional include_resource setting. Argyle stresses that subscriptions "apply to every matching event available to the API key" and are not scoped to one user or account. One subscription for paystubs.added fires for every user you have ever connected.

Argyle does not generate a signing secret. You supply one when you create the subscription, and only then does each delivery carry X-Argyle-Signature, a hex HMAC-SHA512 of the raw body. A subscription without a secret sends no signature header at all.

Argyle webhook features

FeatureDetails
ConfigurationArgyle Console, or POST /v2/webhooks
ScopeEvery user and account under the API key
Signaturehex HMAC-SHA512
Signature headerx-argyle-signature
Signed contentThe raw request body, unparsed
SecretOptional, chosen by you, set on the subscription
BodyJSON
Event routingBody field event
Response window200 within 10 seconds
RetriesOne retry after 30 seconds, only for 429, 500, 502, 503 or 504
Source IPs34.27.251.226, 34.122.50.253, 35.188.163.115, 35.226.53.251
Delivery logNo bodies are logged. The subscription's last_sent_at shows the last attempt

Events and payloads

The top-level event field names the subscribed event as <resource>.<action>. Argyle documents 46 events. These are the account, gig, identity and Item events:

EventTriggered WhenCommon Use Cases
accounts.addedA new account is addedTrack a user's linked payroll accounts
accounts.connectedAn account connectsStart fetching data
accounts.failedAn account connection failsPrompt the user to retry
accounts.removedAn account is removedClean up stored data
accounts.updatedAn account changesRefresh connection state
gigs.added, gigs.updated, gigs.removedGig data changesSync gig records
gigs.partially_synced, gigs.fully_syncedGig sync progresses or completesKnow when data is ready
identities.added, identities.updatedIdentity data changesRefresh identity records
items.updated, items.removedA supported employer or platform changesRefresh your item list

The other families follow the same pattern:

  • Verifications: verifications.updated when a verification's status changes, verifications.creation_failed, and verifications.connection_added when another payroll account or bank is connected after a verification.
  • Paystubs and shifts: added, updated, partially_synced and fully_synced, plus shifts.removed. The added events fire only on a later refresh, not during the initial retrieval.
  • Documents and uploads: payroll_documents.* including ocr_completed and ocr_failed for scanned 1099 and W-2 forms, and user_uploads.* including Ocrolus ocr_completed, ocr_failed and ocr_authenticity.
  • Others: user_forms.submitted and user_forms.removed, ratings.added and ratings.updated, vehicles.*, reports.ready and reports.generated, and users.fully_synced once initial retrieval completes for every newly connected account.

For a verification product, Argyle's advice is to subscribe to verifications.updated and verifications.creation_failed. For the data APIs, subscribe to the account events and the sync events for the data you need.

Event Payload Structure

  • event: the subscribed event name. No header names the type.
  • name: the subscription's own name.
  • data: the ids of the affected objects (the resource itself is included as data.resource when the subscription config sets include_resource).

Published payload example

This is the accounts.connected sample from the webhook samples library. Argyle's reference renders it from the event's OpenAPI schema.

{
  "event": "accounts.connected",
  "name": "name-for-the-webhook-subscription",
  "data": {
    "account": "0187c66e-e7e5-811c-b006-2232f00f426a",
    "user": "018051aa-f7a9-a0db-2f38-6cfa325e9d69"
  }
}

Without include_resource, the body is a pointer: the event name and some IDs. Fetch the account, paystub or verification from the API when you need its contents.

Setting up Argyle webhooks

Generate a secret first, because Argyle won't create one for you:

openssl rand -hex 32

Then create the subscription in Console, or with the API:

{
  "events": ["verifications.updated", "verifications.creation_failed"],
  "name": "verification-status",
  "url": "https://example.com/webhooks/argyle",
  "secret": "<your generated secret>",
  "config": { "include_resource": true }
}

Store the same secret in your app as WEBHOOK_SECRET. It is used as plain text.

Some choices to make at this point:

  • Wildcard or explicit list. "*" subscribes to most events, but Argyle lists exceptions: Item webhooks are not included, partial-sync events can't set their history window through the wildcard, and events that support include_resource must be subscribed to individually if you want the resource object.
  • Resource included or not. include_resource saves an API call per event, at the cost of larger bodies containing personal financial data.
  • One subscription or several. Separate subscriptions with their own names make it easy to route by the body's name field.

HTTP Method

Argyle's docs don't name the HTTP method. Accept GET, POST, PUT, PATCH and DELETE on your route, as the examples do, and verify the signature first for every method.

Hookdeck's Argyle source pre-selects POST without managing the method, so any other method the sender uses must also be allowed on the source.

Rotating the Secret

Update the subscription's secret and WEBHOOK_SECRET together. Deliveries signed with the old value fail with 401 in between, so do it at a quiet time.

To test against Argyle's sandbox from your machine, forward deliveries with the Hookdeck CLI:

npx hookdeck-cli listen 3000 argyle --path /webhooks/argyle

Securing Argyle webhooks

Read the raw body without parsing it, compute an HMAC-SHA512 with the subscription's secret, encode it as hex and compare it with X-Argyle-Signature using a timing-safe comparison. This is the method in Argyle's webhook guide, which also shows a Node sample.

const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-argyle-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha512', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{128}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

The handler accepts any method because Argyle doesn't document one. It verifies first, acknowledges an empty verified body, and stores the event before answering so the single retry is never needed.

const express = require('express');

function createArgyleApp(store) {
  const app = express();
  app.all('/webhooks/argyle', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
    if (!req.body.length) return res.sendStatus(200);

    let parsed;
    try {
      parsed = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return res.sendStatus(400);
    const { event, name, data } = parsed;
    if (typeof event !== 'string') return res.sendStatus(400);
    try {
      // e.g. accounts.connected -> fetch the account; paystubs.fully_synced -> fetch paystubs.
      await store({ event, subscription: name, user: data?.user, account: data?.account, data });
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

Nothing signed is time-bound. Argyle sends no timestamp header, so idempotency is your protection against a replayed request.

Hookdeck's Argyle source type verifies X-Argyle-Signature as a hex HMAC-SHA512 of the raw body, using the secret you set on the subscription. Set a secret on the Argyle webhook subscription. Without one, deliveries are unsigned and fail verification. If you deliberately run an unsigned subscription, leave verification off on the source. Hookdeck answers Argyle with 200 either way, records a failed check as failed verification and does not deliver that request. No time limit applies, so replays verify too.

Make Argyle webhooks production-ready. Hookdeck Event Gateway answers Argyle inside its 10-second window, verifies the SHA-512 signature, and keeps every verification and data event until your service has processed it.

Argyle webhook limitations and pain points

One retry, and only for some failures

The Problem: A brief outage or a slow response loses an event permanently.

Why It Happens: If Argyle doesn't get a 200 within 10 seconds, it retries once after 30 seconds, and only when the response was 429, 500, 502, 503 or 504. A 404 during a bad deploy or a 401 from a misconfigured secret gets no retry at all.

Workarounds:

  • Keep the endpoint simple and fast: verify, store, return 200.
  • Watch last_sent_at on each subscription and compare it with what you received.
  • Reconcile important state, such as verification status, against the API on a schedule.

How Hookdeck Can Help: Hookdeck accepts the delivery on Argyle's first attempt and stores it, then retries your service as many times as you configure, so the one-retry limit stops mattering.

Unsigned unless you remember the secret

The Problem: Requests to your endpoint can't be authenticated, and nothing warned you.

Why It Happens: secret is optional on the subscription and Argyle never generates one. Created without it, the subscription sends deliveries with no X-Argyle-Signature header.

Workarounds:

  • Always set secret when creating subscriptions, including in scripts and Terraform.
  • Fail closed when the header is missing, as the verifier above does.
  • Optionally, restrict the endpoint to Argyle's four static IPs as a second layer.

How Hookdeck Can Help: Hookdeck's Argyle source rejects unsigned deliveries once verification is on, and its request log shows any that arrive without the header.

No body logs on Argyle's side

The Problem: When something goes wrong, there's no record of what Argyle sent.

Why It Happens: Argyle says it does not log webhook request bodies. last_sent_at tells you an attempt happened, not what it contained or what your server replied.

Workarounds:

  • Log the verified body yourself, with care for the personal data it may contain.
  • Keep the event name, user and account IDs from each delivery so you can reconstruct a timeline.

How Hookdeck Can Help: Hookdeck stores each request and your service's response, so you can inspect and replay a delivery after the fact.

Pointers, not data

The Problem: A burst of paystubs.added events turns into a burst of API calls.

Why It Happens: By default the body holds only IDs. Subscribing with "*" also excludes resource objects, because events that support include_resource must be subscribed to individually.

Workarounds:

  • Subscribe explicitly to the events you need with include_resource set.
  • Wait for the fully_synced event before fetching a whole resource type, rather than reacting to every added.

How Hookdeck Can Help: Hookdeck's delivery rate limits let you cap how fast events reach your service, which in turn caps your calls back to Argyle.

Best practices

Wait for the sync events before reading data

After a new account connects, Argyle retrieves data in the background. paystubs.partially_synced means the history window you asked for is available, paystubs.fully_synced means everything is, and users.fully_synced means every newly connected account has finished. Start downstream work from these, not from accounts.connected.

Verify the raw body for every method

Read the bytes untouched with express.raw({ type: '*/*' }), await request.arrayBuffer() or await request.body(), depending on your framework. Because the method is undocumented, run verification for whatever method arrives, and treat a verified empty body as valid.

Deduplicate deliveries

Argyle tells you to build idempotent handlers because a delivery can arrive more than once, and there is no event ID. Use event plus the IDs in data as the key. Our idempotency guide shows how to store the key with the change.

Keep personal data out of logs

Argyle payloads, especially with include_resource, contain income and identity data. Log event names and IDs, not bodies, unless your logging is approved for that data.

Process asynchronously

Return 200 within the 10-second window and do the API calls and business logic afterwards. Asynchronous processing keeps the response time flat however long the work takes.

Conclusion

Argyle's webhooks tell you when payroll data is ready, but they leave a lot to the receiver: you choose the secret, you keep the logs, and you get a single retry. Set a secret on every subscription, wait for the sync events, and make sure your endpoint answers within 10 seconds every time.

Hookdeck Event Gateway adds the retries, logs and replay that Argyle leaves out, and verifies each SHA-512 signature on the way in.

Get started with Hookdeck for free and handle Argyle webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.