Sharing first-party transactions
Overview
In addition to Bill Manager detecting and managing bills from externally linked accounts, first-party transaction data can also be shared and used to reflect bills paid from internal accounts.
Transactions can be sent for multiple users without a link token needing to be created. Each transaction should be tied to an end_user_id who has or will have a link token created for Bill Manager.
Formatting transactions
Transactions should be collected into TSV files with the following fields in this order, without a header:
transaction_id– your unique identifier for the transactionend_user_id– your unique identifier for the account holderaccount_name– a display name for the accountaccount_type– one of “depository”, “credit”, “other”account_subtype– eg, "checking", "savings"account_mask– the masked account number. This should be the last 4-8 digits of the account number to differentiate it to the holdertransaction_datetime– timestamp (UTC) when the transaction settledaccount_id– your unique identifier for the accountamount– decimal amount. This should be a negative number for outgoings and positive for incomingscurrency_code(optional) – this should be a 3 letter ISO 4217 code. Defaults to “USD” if blankdescription– the transaction descriptioncountry_code(optional) – this should be a 2 letter ISO 3166-1 alpha-2 code. Defaults to “US” if blankpayment_channel(optional) – one of “online”, “in store”, “other”. Defaults to “other” if blanktransfer_type(optional) – "debit" (-) or "credit" (+). This overrides the signage ofamounttransaction_status– one of “pending”, “posted”, “settled”, “funds_available”, “cancelled”, “failed”, “returned”. As updating transactions is not currently supported it is recommended that only final state transactions be included.posted_datetime– timestamp (UTC) when the transaction postedmerchant_category_code– 4 digit merchant category codeenriched_transaction_details(optional) – this should be a JSON object of any additional details related to the transaction. Use the equivalent ofjq -ccompact form.
Rows can be best processed when grouped by end_user_id, and the tuple of account data. Groupings are best processed when sorted by transaction_datetime.
Example six transaction TSV in this format
cb0efb74-5c8c-4388-931d-24bd66514191 01JVAWFJBDH56425F35VQYQ8T0 Basic Checking depository checking 940316 2025-03-22T00:15:49Z 3a14519b-e78e-4e5e-8ebe-ad30f6ab36fc 828.61 USD CASH APP*KALISTA HOWELL800-9691940 CAUS US settled 2025-03-22T03:57:11Z 5373 {"txn_uuid":"cb0efb74-5c8c-4388-931d-24bd66514191","account_uuid":"243e88f2-4ba8-45ef-84c1-cdd53bd19307","end_user_uuid":"b09d6d65-168e-4bac-882e-2e10671bc766"}
c456d28b-8f2c-406e-a5dc-d2c2763bde3e 01JVAWFJBDH56425F35VQYQ8T0 Basic Checking depository checking 940316 2025-03-23T13:09:54Z 3a14519b-e78e-4e5e-8ebe-ad30f6ab36fc 1206.09 USD CASH APP*CODY ENNIS*ADD800-9691940 CAUS US online settled 2025-03-24T20:25:06Z 5091 {"txn_uuid":"0456d28b-8f2c-406e-a5dc-d2c2763bde3e","account_uuid":"243e88f2-4ba8-45ef-84c1-cdd53bd19307","end_user_uuid":"b09d6d65-168e-4bac-882e-2e10671bc766"}
ac5b680e-56ef-4fb4-86ca-c921d6976031 01JVAWFJBDH56425F35VQYQ8T0 Basic Checking depository checking 940316 2025-03-25T15:45:51Z 3a14519b-e78e-4e5e-8ebe-ad30f6ab36fc 1705.10 USD TICKPICK NEW YORK NYUS US online settled 2025-03-28T11:34:56Z {"txn_uuid":"ac5b680e-56ef-4fb4-86ca-c921d6976031","account_uuid":"243e88f2-4ba8-45ef-84c1-cdd53bd19307","end_user_uuid":"b09d6d65-168e-4bac-882e-2e10671bc766"}
a596a06a-2051-4c85-9d4d-2af8bd989e2a 01JVAWMQ79QS7E64JRD6TC5Z33 Basic Savings depository savings 2715 2023-01-03T10:32:35Z 0647ce91-948b-4682-aebf-9030e7bdd923 1869.60 USD LYFT *TEMP AUTH HOLD +18552800278 CAUS US other debit settled 2023-01-03T21:37:50Z {"txn_uuid":"7596a06a-2051-4c85-9d4d-2af8bd989e2a","account_uuid":"b3c909b9-cdf5-45e2-87ba-3be7a2814a3a","end_user_uuid":"03f9ad37-e83e-452d-b543-51485e9caa34"}
a5355f4b-85a5-4418-85b7-9b7ea80c3f47 01JVAWMQ79QS7E64JRD6TC5Z33 Basic Savings depository savings 2715 2023-01-03T12:59:24Z 0647ce91-948b-4682-aebf-9030e7bdd923 728.06 USD APPLE.COM/BILL 866-712-7753 CAUS US in store settled 2023-01-04T12:06:42Z 3945 {"txn_uuid":"5a355f4b-85a5-4418-85b7-9b7ea80c3f47","account_uuid":"b3c909b9-cdf5-45e2-87ba-3be7a2814a3a","end_user_uuid":"03f9ad37-e83e-452d-b543-51485e9caa34"}
de012c23-19b0-44c1-a4db-f7ef0fc412f2 01JVAWMQ79QS7E64JRD6TC5Z33 Basic Savings depository savings 2715 2023-01-03T20:36:43Z 0647ce91-948b-4682-aebf-9030e7bdd923 1033.14 USD CASH APP*JACQUELYN HILL800-9691940 CAUS US other settled 2023-01-04T01:46:18Z {"txn_uuid":"de012c23-19b0-44c1-a4db-f7ef0fc412f2","account_uuid":"b3c909b9-cdf5-45e2-87ba-3be7a2814a3a","end_user_uuid":"03f9ad37-e83e-452d-b543-51485e9caa34"}Posting transactions to Pinwheel
A single batch can consist of multiple files to upload. Compressed file sizes must not exceed 100MB. With your Pinwheel API key, create a new batch with the following endpoint:
POST https://api.getpinwheel.com/v1/gateway/transactions_batch/files
The body of the request will be a JSON schema (see example below) with the following fields:
date_range_start– ISO 8601 formatted minimumtransaction_datetimeacross files in the batchdate_range_end– ISO 8601 formatted maximumtransaction_datetimeacross files in the batch.file_count– an integer count of how many files you will upload in this batch. Max 100.
{
"data":{
"date_range_start": "2025-01-01T00:00:00Z",
"date_range_end": "2025-12-31T00:00:00Z",
"file_count": 1
}
}You will get a response including the batch_id, the upload_urls to which you can post the files to, and expires_at time. The URLs will be valid for just under 5 minutes given round-trip time.
> … send … >
POST /v1/gateway/transactions_batch/files
Host: api.getpinwheel.com
Content-Type: application/json
Pinwheel-Version: 2025-07-08
x-api-secret: YOUR-API-SECRET
{
"data": {
"date_range_start": "2025-01-01T00:00:00Z",
"date_range_end": "2025-12-31T00:00:00Z",
"file_count": 2
}
}
< … example response … <
{
"data": {
"batch_id": "0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed",
"upload_urls": [
"https://….s3.amazonaws.com/upload/batched_transactions/ffffffff-ffff-ffff-ffff-ffffffffffff/2025-01-01_2025-12-31_0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed_002_001.tsv.gz?AWSAccessKeyId=AS…&Signature=KIok…ndEU%3D&content-type=text%2Ftab-separated-values&x-amz-security-token=IQoJb3JpZ2luX……%3D%3D&Expires=1760115099",
"https://….s3.amazonaws.com/upload/batched_transactions/ffffffff-ffff-ffff-ffff-ffffffffffff/2025-01-01_2025-12-31_0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed_002_002.tsv.gz?AWSAccessKeyId=AS…&Signature=fYav…6ig%3D&content-type=text%2Ftab-separated-values&x-amz-security-token=IQoJb3JpZ2luX…%3D%3D&Expires=1760115099"
],
"expires_at": "2025-10-10T16:51:39Z"
}
}You will then post your transaction batch files to the upload_urls provided.
Kicking off transaction data ingestion
Once all of your file uploads are complete, use the POST https://api.getpinwheel.com/v1/gateway/transactions_batch/complete endpoint to signal to Pinwheel that the data is ready to be ingested and validated.
Provide the following JSON payload with your request.
Note: if not all files have been successfully uploaded, this endpoint will return a 400 error and a list of files with statuses, see example below. Files still in pending status have not been recognized as uploaded.
POST /v1/gateway/transactions_batch/complete
Host: api.getpinwheel.com
Content-Type: application/json
Pinwheel-Version: 2025-07-08
x-api-secret: YOUR-API-SECRET
{
"data": {
"batch_id": "0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed"
}
}
< … example response … <
{
"data": {
"detail": "ok"
}
}
POST /v1/gateway/transactions_batch/complete
Host: api.getpinwheel.com
Content-Type: application/json
Pinwheel-Version: 2025-07-08
x-api-secret: YOUR-API-SECRET
{
"data": {
"batch_id": "0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed"
}
}
< … example response … <
{
"data": {
"detail": "Not all files have successfully uploaded",
"files": [{
"file_id": "6451d300-f8ff-49ee-adab-af666468544f",
"filename": "2025-01-01_2025-12-31_0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed_002_001.tsv.gz",
"status": "pending", // file not yet uploaded
"error_message": null,
"uploaded_at": null,
"validated_at": null,
}]
}
}Checking Ingestion Status
With your Pinwheel API key, use this API call to check the status of the ingestion and receive any errors that occurred per file:
GET https://api.getpinwheel.com/v1/gateway/transactions_batch/files?batch_id={batch_id}&include=errors
Poll this endpoint once every 3-5 seconds until the batch status is in one of the terminal values - either completed, invalid, or failed.
Batch and file statuses
Batch status can be one of:
uploading- not all files are uploaded yet.ingesting- all files are uploaded and ingestion is in progress.completed- all files have been ingested without error. All files will have the statusingested.invalid- one or more files failed validation and have the statusinvalid.failed- a system error occurred during ingestion, and one or more files will have statusfailed.
File status can be one of:
pending- upload URL created for the file, but it hasn't been uploaded yet.uploaded- file has been uploaded.loading- file contents are being loaded and checked for basic correctness.loaded- file has passed basic correctness checks and ready for validation.ingesting- file is being ingested and validated row-by-row.ingested- file has been ingested with no validation errors.invalid- file has one or more validation errors.failed- ingesting the file failed due to a system error.
Example responses
> … send … >
GET /v1/gateway/transactions_batch/files?batch_id=0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed&include=errors
Host: api.getpinwheel.com
Content-Type: application/json
Pinwheel-Version: 2025-07-08
x-api-secret: YOUR-API-SECRET
< … example response … <
{
"data": {
"batch_id": "0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed",
"status": "completed",
"files": [{
"file_id": "6451d300-f8ff-49ee-adab-af666468544f",
"filename": "2025-01-01_2025-12-31_0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed_002_001.tsv.gz",
"status": "ingested",
"error_message": null,
"uploaded_at": "2026-01-01T16:41:39Z",
"validated_at": "2026-01-01T16:42:13Z",
}],
"created_at": "2026-01-01T00:00:00Z",
"started_at": "2026-01-01T16:42:00Z",
"completed_at": "2026-01-01T16:42:13Z",
"error_message": null
}
}> … send … >
GET /v1/gateway/transactions_batch/files?batch_id=0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed&include=errors
Host: api.getpinwheel.com
Content-Type: application/json
Pinwheel-Version: 2025-07-08
x-api-secret: YOUR-API-SECRET
< … example response … <
{
"data": {
"batch_id": "0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed",
"status": "invalid",
"files": [{
"file_id": "6451d300-f8ff-49ee-adab-af666468544f",
"filename": "2025-01-01_2025-12-31_0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed_002_001.tsv.gz",
"status": "invalid",
"error_message": "1 validation error(s)",
"errors": [
"row_number": 1,
"stage": "validation",
"error_type": "missing_field",
"message": "end_user_id is required",
],
"uploaded_at": "2026-01-01T16:41:39Z",
"validated_at": null,
}],
"created_at": "2026-01-01T00:00:00Z",
"started_at": "2026-01-01T16:42:00Z",
"completed_at": null,
"error_message": "Batch had errors (status is invalid); see files.errors."
}
}> … send … >
GET /v1/gateway/transactions_batch/files?batch_id=0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed&include=errors
Host: api.getpinwheel.com
Content-Type: application/json
Pinwheel-Version: 2025-07-08
x-api-secret: YOUR-API-SECRET
< … example response … <
{
"data": {
"batch_id": "0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed",
"status": "failed",
"files": [{
"file_id": "6451d300-f8ff-49ee-adab-af666468544f",
"filename": "2025-01-01_2025-12-31_0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed_002_001.tsv.gz",
"status": "failed",
"error_message": null,
"uploaded_at": "2026-01-01T16:41:39Z",
"validated_at": null,
}],
"created_at": "2026-01-01T00:00:00Z",
"started_at": "2026-01-01T16:42:00Z",
"completed_at": null,
"error_message": "Batch had errors (status is failed); see files.errors."
}
}Batch Ingestion Outcomes
Happy path
In the happy path, the batch status will be completed. At this stage, we are already working to identify recurring bills and subscriptions in the transaction data.
Server/Runtime Errors
If our ingestion process fails for a reason not related to validation errors, the batch status will be set to failed and one or more files will be set to failed.
If the batch status is failed, we recommend waiting for a bit and then calling the "complete" endpoint again, as described above:
POST https://api.getpinwheel.com/v1/gateway/transactions_batch/complete
This will automatically restart the ingestion process. If the runtime error was transient, then things will proceed normally. If the error persists, please reach out to our customer success team.
Correcting and Re-uploading Invalid Transaction Files
If one or more transaction files have validation errors, the batch status will be invalid, and one or more files will have the status invalid.
If aninvalidbatch includes multiple files, and there are files that do not have any validation errors, they will be ingested normally and do not need to be re-uploaded.
During ingestion, we collect validation errors up to a maximum of 100 errors. Any files in the invalid state will have an errors list that details each error. These error descriptions should have enough detail to help you correct the transaction files.
For each corrected file, call the "reupload" endpoint to retrieve a new presigned URL for upload:
POST https://api.getpinwheel.com/v1/gateway/transactions_batch/files/reupload
The body of the request will be a JSON schema (see example below) that includes the batch_id and file_id from the status API result.
{
"data": {
"batch_id": "0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed",
"file_id": "6451d300-f8ff-49ee-adab-af666468544f",
}
}The response of this endpoint provides a new upload_url and expires_at timestamp.
POST /v1/gateway/transactions_batch/files/reupload
Host: api.getpinwheel.com
Content-Type: application/json
Pinwheel-Version: 2025-07-08
x-api-secret: YOUR-API-SECRET
{
"data": {
"batch_id": "0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed",
"file_id": "6451d300-f8ff-49ee-adab-af666468544f",
}
}
< … example response … <
{
"data": {
"upload_url": "https://….s3.amazonaws.com/upload/batched_transactions/ffffffff-ffff-ffff-ffff-ffffffffffff/2025-01-01_2025-12-31_0199c9e2-a9bf-7c9e-b426-c60a8b4b09ed_002_001.tsv.gz?AWSAccessKeyId=AS…&Signature=KIok…ndEU%3D&content-type=text%2Ftab-separated-values&x-amz-security-token=IQoJb3JpZ2luX……%3D%3D&Expires=1760115099",
"expires_at": "2025-10-10T16:51:39Z"
}
}
Upload the corrected file to its upload_url.
Repeat this for each file that needs to be corrected. Once all files are uploaded, call the "complete" endpoint again as described earlier, and poll the ingestion status endpoint as before.
Note that only the re-uploaded files will be re-processed. As noted above, any files that passed validation will have already been ingested.