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 transaction
  • end_user_id – your unique identifier for the account holder
  • account_name – a display name for the account
  • account_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 holder
  • transaction_datetime – timestamp (UTC) when the transaction settled
  • account_id – your unique identifier for the account
  • amount – decimal amount. This should be a negative number for outgoings and positive for incomings
  • currency_code (optional) – this should be a 3 letter ISO 4217 code. Defaults to “USD” if blank
  • description – the transaction description
  • country_code (optional) – this should be a 2 letter ISO 3166-1 alpha-2 code. Defaults to “US” if blank
  • payment_channel (optional) – one of “online”, “in store”, “other”. Defaults to “other” if blank
  • transfer_type (optional) – "debit" (-) or "credit" (+). This overrides the signage of amount
  • transaction_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 posted
  • merchant_category_code – 4 digit merchant category code
  • enriched_transaction_details (optional) – this should be a JSON object of any additional details related to the transaction. Use the equivalent of jq -c compact 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 minimum transaction_datetime across files in the batch
  • date_range_end – ISO 8601 formatted maximum transaction_datetime across 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 status ingested.
  • invalid - one or more files failed validation and have the status invalid.
  • failed - a system error occurred during ingestion, and one or more files will have status failed.

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 an invalid batch 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.