Work logs and rate cards
Capture structured contractor work with Wingspan V3 work logs, price it with rate cards and rate calculations, and turn approved work into a payable.
This guide shows you how to record contractor work as structured entries, have Wingspan calculate what each entry is worth, and turn approved work into a payable. It covers both setup (what to measure and how to price it) and the day-to-day flow (log, submit, approve).
Use work logs when pay depends on what someone did: hours, visits, units, or a mix. If you already know the amount, create a payable directly.
How the pieces fit
| Resource | What it is | Path |
|---|---|---|
| Work definition | The fields each entry must capture, such as hours or visitType, with validation rules. | /v3/payments/work-definitions |
| Rate card | Named prices, such as an hourly rate. | /v3/payments/rate-cards |
| Rate calculation | The formula that turns an entry's fields and the rate card into an amount. | /v3/payments/rate-calculations |
| Engagement | The reusable engagement template. It attaches work definitions (each with a rate calculation) and a base rate card. | /v3/payments/engagements |
| Payee engagement | One payee's assignment under that engagement. It can override the rate card with that payee's own rates. | /v3/payments/payees/{payeeId}/engagements |
| Work log | A container of entries for one payee engagement, with a review workflow. | /v3/payments/work-logs |
| Work item | One entry of work, validated against a work definition and priced automatically. | /v3/payments/work-items |
flowchart LR
WD[Work definition] --> ENG[Engagement]
RC[Rate calculation] --> ENG
CARD[Rate card] --> ENG
ENG --> PE[Payee engagement]
PE --> WL[Work log]
WI[Work items] --> WL
WL -->|approve| P[Payable]
In V1 the payee engagement was called a Payer-Payee-Engagement (PPE), and a work log was converted with POST /payments/work-log/{id}/convert. In V3, approving the work log creates the payable.
Setup
You do setup once per type of work. The example pays hourly consulting at $75 an hour.
1. Create a work definition
POST /v3/payments/work-definitions declares the fields each entry must have.
curl -X POST "https://api.wingspan.app/v3/payments/work-definitions" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Consulting hours",
"externalId": "WD-CONSULT",
"attributeDefinitions": [
{ "key": "hours", "name": "Hours worked", "type": "Number", "isRequired": true, "validationRules": { "minimum": 0, "maximum": 24 } },
{ "key": "clientCode", "name": "Client code", "type": "String", "isRequired": false }
]
}'Attribute type is Boolean, String, Number, Datetime, or ValueSet. validationRules supports minimum and maximum for numbers, regex for strings, and an enumeration config for value sets.
Keep definitions focused on what drives pay and what you need for review. Every required field is one more thing the person logging work has to enter.
2. Create a rate card
POST /v3/payments/rate-cards holds the prices.
curl -X POST "https://api.wingspan.app/v3/payments/rate-cards" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Consulting standard",
"currency": "USD",
"rates": [ { "description": "Hourly rate", "unitType": "Hour", "unitCost": 75.00 } ]
}'// (trimmed)
{
"id": "Rc7Hx3Pn9Wk2Tz5Lq8Mv1F",
"currency": "USD",
"rates": [ { "id": "Tr1Kx5Wq8Zm3Lv7Rn2Pb6C", "description": "Hourly rate", "unitType": "Hour", "unitCost": 75.00 } ]
}unitType is Hour, Day, Week, Month, Unit, or Fixed. Wingspan assigns each rate an id; your formula refers to it in the next step.
3. Create a rate calculation
POST /v3/payments/rate-calculations defines the formula. Placeholders connect the two sides: ${workItem.<key>} is a field from the entry, and ${rateCard.<rateId>} is a rate's unitCost from the payee's resolved rate card.
curl -X POST "https://api.wingspan.app/v3/payments/rate-calculations" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Hours times hourly rate",
"resultStrategy": "SingleGroup",
"calculationGroups": [
{
"id": "base",
"selectionStrategy": "Sum",
"calculations": [
{
"id": "hourly",
"selectionCriteria": "!!(${workItem.hours})",
"formula": "${workItem.hours} * ${rateCard.Tr1Kx5Wq8Zm3Lv7Rn2Pb6C}",
"description": "Hours worked at the hourly rate"
}
]
}
]
}'- Each row has a
selectionCriteriagate and aformula. A row counts only when its gate is true. selectionStrategy: Sumadds every matching row, for pay made of several categories.Firsttakes the first matching row, for mutually exclusive cases such as one rate per visit type.- Formulas allow arithmetic operators, numbers, parentheses, and placeholders. No functions.
- A missing field or a formula that can't be evaluated fails with
422or holds the entry for review. It never quietly evaluates to zero. - Today, only
resultStrategy: SingleGroupwith exactly one group is supported. Group and rowidvalues are yours to choose: 1 to 64 letters, digits, underscores, periods, or hyphens.
4. Attach everything to the engagement
Give the engagement its base rate card (rateCardId on POST /v3/payments/engagements or its PATCH), then attach the work definition with its rate calculation:
curl -X POST "https://api.wingspan.app/v3/payments/engagements/Eg1Tn6Wq3Zx9Kv4Lr7Pm2D/work-definitions" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "workDefinitionId": "Dn5Vk8Rt2Xq6Lm1Zw9Hb4S", "rateCalculationId": "Ca4Qz8Lt1Nx6Vw3Rk9Bm2J" }'Payees are then assigned to the engagement through their payee engagements. See Engagements.
5. Give one payee different rates (optional)
To pay one payee a different rate, bind a rate card to their payee engagement with PATCH /v3/payments/payees/{payeeId}/engagements/{payeeEngagementId}/rate-card, body { "rateCardId": "..." }. The payee's card is merged with the engagement's base card, and the payee's card wins. The rate card must belong to your Account.
6. Choose workflow settings
PATCH /v3/payments/work-logging-configs sets Account-wide options. Each takes { "enabled": true } or { "enabled": false }.
| Setting | What it does |
|---|---|
allowPayeeCreatedWorkLogs | Payees can create their own work logs. |
autoApproveWorkLogs | Work logs skip review and go straight from Draft to Approved. |
requireAttachments | A work log needs at least one attachment before it can be submitted. |
Payees can read your settings at GET /v3/payments/work-logging-configs/payers/{payerAccountId}.
Day-to-day: log, submit, approve
1. Create a work log
Create one work log per payee engagement per period with POST /v3/payments/work-logs. As the payer you can log work for a payee; if you allow it, payees can create their own.
curl -X POST "https://api.wingspan.app/v3/payments/work-logs" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "payeeEngagementId": "Tz3Nq8KpW1vXr6Ld0Ya5Mc", "currency": "USD", "externalId": "TS-2026-W39-PRIYA" }'// (trimmed)
{ "id": "Lg2Wm7Qx4Tb9Kz1Rn6Vp3C", "status": "Draft", "payeeEngagementId": "Tz3Nq8KpW1vXr6Ld0Ya5Mc", "workLogNumber": "WL-0107", "totalAmount": 0 }periodStart, periodEnd, totalHours, and totalAmount are calculated from the entries; you don't send them.
2. Add work items
Add each entry with POST /v3/payments/work-items. attributes must match the work definition.
curl -X POST "https://api.wingspan.app/v3/payments/work-items" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"workLogId": "Lg2Wm7Qx4Tb9Kz1Rn6Vp3C",
"workDefinitionId": "Dn5Vk8Rt2Xq6Lm1Zw9Hb4S",
"timestamp": "2026-09-22T17:00:00Z",
"attributes": { "hours": 6.5, "clientCode": "NW-114" }
}'// (trimmed)
{
"id": "It3Lx9Qv6Wz2Kn8Rt1Mb5P",
"status": "Active",
"attributes": { "hours": 6.5, "clientCode": "NW-114" },
"calculations": {
"result": 487.50,
"selectionStrategy": "Sum",
"items": [ { "result": 487.50, "formula": "${workItem.hours} * ${rateCard.Tr1Kx5Wq8Zm3Lv7Rn2Pb6C}", "interpolatedFormula": "6.5 * 75" } ]
}
}calculations shows the result and how it was reached, so both sides can see exactly how pay was calculated. The work log's totals update on every change. Fix an entry with PATCH /v3/payments/work-items/{workItemId}, or remove it with DELETE. Either side can leave a comment on an entry through payerOwnedData.comment or payeeOwnedData.comment on the update.
List a log's entries with GET /v3/payments/work-items?filter[workLogId][eq]=Lg2Wm7Qx4Tb9Kz1Rn6Vp3C.
3. Submit for review
POST /v3/payments/work-logs/{workLogId}/submit moves the log from Draft (or InProgress) to InReview.
4. Review
As the payer, do one of:
- Approve.
POST /v3/payments/work-logs/{workLogId}/approvemoves the log toApprovedand creates its output: a payable for a contractor engagement, or a pay statement for an employee engagement. The response'sconvertedResourceTypeandconvertedResourceIdpoint to it. Calling approve again doesn't create a second payable. - Ask for changes.
POST /v3/payments/work-logs/{workLogId}/request-editwith an optionalreasonmoves the log toInProgressso the payee can fix it and resubmit. - Reject.
POST /v3/payments/work-logs/{workLogId}/rejectwith a requiredreasonmoves it toRejected. Rejection is final; the payee creates a new work log.
Comment on a work log
Either side can leave one comment on the work log itself with POST /v3/payments/work-logs/{workLogId}/comment, body { "comment": "..." }. It works while the log is Draft, InProgress, or InReview, and doesn't change the status. An Approved or Rejected log returns 409.
- Wingspan works out from your Account whether you're writing the payer or payee comment. The log shows them as
payerOwnedDataandpayeeOwnedData. - Each call replaces your side's existing comment. An empty string clears it.
- A payee comment on an
InReviewlog asks the payer for edits and notifies them. Clearing the comment withdraws the request. - Both comments are cleared on every status change. Earlier comments stay in the log's event history.
5. Pay
Read the payable at convertedResourceId and pay it like any other: directly, or in a payroll run.
Work log statuses
| Status | Meaning |
|---|---|
Draft | Being filled in. Entries can be added and changed. |
InProgress | Sent back by the payer for edits. |
InReview | Submitted and waiting on the payer. |
Approved | Approved. The payable or pay statement has been created. |
Rejected | Rejected. Final. |
A Draft work log can be deleted. Once submitted or approved, delete returns 409. PATCH /v3/payments/work-logs/{workLogId} changes notes, attachmentIds, and metadata.
List logs with GET /v3/payments/work-logs, filtered by payeeEngagementId, payeeAccountId, payerAccountId, payPeriodId, payrollRunId, status, or externalId.
Work log webhooks aren't subscribable yet. Poll the work log, or subscribe to Payable.Paid for the payable it produces.
Common mistakes
- Writing a formula with a rate name instead of its ID.
${rateCard.<rateId>}uses the rate'sidfrom the rate card response. - Changing a rate calculation that's in use. Changing
calculationGroupsreturns409while any engagement's work definition still uses it. Create a new calculation and attach that instead. - Deleting in-use setup. A rate card used by a payee engagement, or a work definition attached to an engagement, can't be deleted (
409). - Expecting approval to pay. Approving creates the payable. You still pay it.
Related pages
Updated 10 days ago