API Reference

The Trailing Paper API gives your store, POS system, or custom application direct access to card payments, invoicing, and payout data for your Trailing Paper merchant account. The API is built on REST principles — all requests and responses use JSON, authentication is via a Bearer token, and standard HTTP status codes indicate success or failure.

This reference documents every available endpoint with full request/response schemas, code examples in four languages, and detailed error descriptions.

Who this is for

This API is for merchants and developers building integrations on top of a Trailing Paper account. Common use cases:

  • Charging a customer's card from a custom checkout or e-commerce storefront
  • Creating and sending invoices programmatically from ERP or accounting software
  • Pulling payout and settlement data into business intelligence dashboards
  • Automating customer record creation and payment history lookups

Approved merchant account required. Your Trailing Paper account must be approved by Finix before API charges will succeed. Charges attempted on an unapproved account return 422. Check your account status in your merchant dashboard.

Base URL

https://trailingpaper.com/api

API versioning

The current API version is v1. Breaking changes will be introduced only with advance notice via email to the address on your account. Non-breaking additions (new fields, new endpoints) may be made at any time.


Authentication

All API requests are authenticated with an API key passed as a Bearer token in the Authorization header. Your API key provides full access to your merchant account — never expose it in client-side code, public repositories, or browser requests.

Authorization: Bearer tp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Getting your API key

API keys are managed in your dashboard → API Keys. You can create multiple keys with distinct labels (one per integration), see when each was last used, and revoke any key individually without affecting others.

One-time display. Your full key is shown only once, immediately after creation. Copy it and store it in a secret manager (e.g. AWS Secrets Manager, Vault, a .env file outside your repo). If you lose access to a key, revoke it and generate a new one.

Key format

PrefixLengthExample
tp_live_ 56 chars total tp_live_a3f8c2d...

Unauthenticated requests

Any request with a missing, malformed, or revoked API key returns 401 Unauthorized:

HTTP/2 401

{ "error": "Invalid or inactive API key." }

Environments

All API requests currently target the live production environment. A sandbox environment with test card numbers and no real money movement is planned for a future release.

EnvironmentBase URLStatus
Production https://trailingpaper.com/api Active
Sandbox https://sandbox.trailingpaper.com/api Coming soon

Live charges only. Until a sandbox is available, all API charges process real card transactions through Finix and settle real funds. Do not use production cards for testing.


Making Requests

Required headers

HeaderValue
Authorization Required Bearer tp_live_...
Content-Type POST only application/json
Idempotency-Key Recommended Any unique string. UUID v4 recommended. See Idempotency.

Request body

POST request bodies must be JSON-encoded. application/x-www-form-urlencoded and multipart are not supported. Omitting Content-Type: application/json on a POST will result in a 400 response.

Response format

All responses are JSON. Successful responses return the resource object. Error responses always include an error key with a human-readable description of the problem. Field-level validation errors may return error as an object keyed by field name.

HTTPS only

All API requests must use HTTPS. HTTP requests will be rejected or redirected. TLS 1.2+ is required.


Idempotency

POST requests to the API — especially /api/charge — support idempotent retries via the Idempotency-Key header. If a network failure, timeout, or unexpected disconnect prevents you from receiving a response, you can retry the exact same request with the same key and be guaranteed no duplicate charge is created.

How it works

When you send a request with an Idempotency-Key, the server stores the response against that key. If you retry within 24 hours, the stored response is returned immediately without reprocessing the request. After 24 hours the key expires and can be reused for a new unrelated request.

Tie keys to orders, not requests. Use a stable identifier from your system — such as order_8742 or a UUID stored in your order record — rather than generating a new UUID per attempt. This lets you retry safely regardless of how many times the network failed.

Format

Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

Any string up to 255 characters is accepted. UUID v4 format is recommended to guarantee uniqueness across your system.


Errors & Status Codes

The API uses conventional HTTP status codes. Codes in the 2xx range indicate success. Codes in the 4xx range indicate a problem with your request. Codes in the 5xx range indicate an error on our end.

200
OKThe request succeeded. The resource is returned in the body.
201
CreatedA new resource was created. The created object is returned in the body.
400
Bad RequestThe request body is malformed, missing required fields, or contains an invalid value. The error field describes what failed.
401
UnauthorizedYour API key is missing, malformed, inactive, or has been revoked. Check your Authorization header.
404
Not FoundThe requested resource does not exist or does not belong to your merchant account.
422
Unprocessable EntityThe request was structurally valid but could not be completed. Most commonly: card declined, payment instrument invalid or expired, or merchant account not yet approved by Finix.
429
Too Many RequestsYou exceeded the rate limit (100 requests/min per key). The Retry-After response header indicates how many seconds to wait before retrying.
500
Internal Server ErrorAn unexpected error occurred on our end. If this persists after a retry, contact support@trailingpaper.com with the approximate timestamp.

Error response shape

All non-2xx responses return a consistent error envelope. Validation errors may return the error value as an object:

// Single error
{ "error": "Card was declined." }

// Field-level validation
{ "error": { "amount": "The amount field is required." } }

Rate Limits

API requests are rate-limited to 100 requests per minute per API key. Requests that exceed this limit receive 429 Too Many Requests with a Retry-After header.

LimitWindowScope
100 requests Per minute Per API key

Handling 429 responses

Implement exponential backoff when you receive a 429. Read the Retry-After header (in seconds) and wait at least that long before retrying. Do not hammer the API — multiple rapid retries will extend your lockout window.

Higher limits

If your integration requires throughput beyond 100 req/min, contact support@trailingpaper.com to discuss a higher-limit arrangement.


Create a Charge

POST https://trailingpaper.com/api/charge Live

Creates a payment transfer against a tokenized card. The charge flows through Finix and settles to your linked bank account on the standard payout schedule (typically T+2 business days). You must have an approved merchant account for charges to succeed.

Amounts are specified in the smallest currency unit — cents for USD. A charge of $99.99 is "amount": 9999. The minimum chargeable amount is $0.50 (50).

A newly created charge starts in PENDING state. It transitions to SUCCEEDED or FAILED asynchronously, typically within a few seconds. Use webhooks to receive real-time state-change notifications rather than polling.

Card tokenization is handled client-side. Never send raw card numbers (PAN, CVV, expiry) to the Trailing Paper API or your own server. Collect card details using Finix.js in the browser, which returns a payment_instrument_id you can safely pass to this endpoint.

Request

Headers

HeaderValue
Authorization Required Bearer tp_live_...
Content-Type Required application/json
Idempotency-Key Recommended UUID v4 tied to your order ID. Prevents duplicate charges on retry.

Body parameters

ParameterTypeDescription
amount
Required
integer Charge amount in the smallest currency unit. For USD, this is cents. Min: 50 ($0.50). Max: 99999999 ($999,999.99). Example: 9999 = $99.99.
payment_instrument_id
Required
string Finix Payment Instrument ID representing a tokenized card. Obtained from Finix.js client-side tokenization. Instrument IDs begin with PI.
currency
Optional
string ISO 4217 currency code. Currently only USD is supported. Defaults to USD if omitted.
description
Optional
string Internal note or reference (e.g. "Order #1234"). Visible in your dashboard and included in webhook payloads. Max 255 characters.

Response — 201 Created

Returns the created transfer object on success.

FieldTypeDescription
success boolean Always true on a 201 response.
transfer_id string Finix transfer ID (prefixed TR). Use this to identify the payment in your dashboard, dispute lookups, or webhook matching.
status string Initial state is always PENDING. Transitions asynchronously to SUCCEEDED or FAILED. Subscribe to webhooks to receive state change events.
amount integer The charged amount in cents, echoed from the request.
currency string The currency code (USD).

Error responses

400
Validation errorA required field is missing, the amount is zero or negative, or a field exceeds its maximum length. The error object will identify the specific field.
401
Invalid API keyThe Bearer token is missing, malformed, or has been revoked.
422
Payment failedCard was declined, the payment instrument is expired or invalid, or your merchant account is not yet approved. Check the error message for the specific reason.
429
Rate limitedSlow down and retry after the number of seconds specified in the Retry-After header.

List Payments

GET https://trailingpaper.com/api/payments Coming Soon

Returns a paginated list of all charges made through your merchant account, sorted by creation time descending. Supports filtering by status, date range, and amount.

In Development

This endpoint is being built. Email us if you need early access.


Retrieve a Payment

GET https://trailingpaper.com/api/payments/{transfer_id} Coming Soon

Returns a single payment transfer by Finix transfer ID. Use this to check the current status of a charge after creation.

In Development

Until this endpoint is available, use webhooks to track state transitions from PENDINGSUCCEEDED.


Tokenize a Card

POST https://trailingpaper.com/api/payment-instruments Coming Soon

Tokenizes a customer's payment details and returns a payment_instrument_id for use with Create a Charge. Raw card data must never pass through your servers.

Use Finix.js in the meantime. Finix's client-side JS library lets you securely collect and tokenize card details in the browser. The resulting payment_instrument_id is accepted by POST /api/charge today. See finix.com/docs.

In Development

Native Trailing Paper card tokenization is coming. Until then, use Finix.js directly.


Create an Invoice

POST https://trailingpaper.com/api/invoices Coming Soon

Creates an invoice and optionally sends it to the customer by email. Returns the invoice object including a unique hosted payment link the customer can use to pay by card.

In Development

Contact us for early access.


List Invoices

GET https://trailingpaper.com/api/invoices Coming Soon

Returns a paginated list of all invoices for your account, with filters for status (draft, sent, paid, overdue) and date range.

In Development

Contact us for early access.


Retrieve an Invoice

GET https://trailingpaper.com/api/invoices/{invoice_id} Coming Soon

Returns a single invoice by ID, including line items, payment status, and the hosted payment URL.

In Development

Contact us for early access.


Create a Customer

POST https://trailingpaper.com/api/customers Coming Soon

Creates a customer record in your Trailing Paper account. Customers can be attached to invoices and used to look up payment history and stored payment instruments.

In Development

Contact us for early access.


List Customers

GET https://trailingpaper.com/api/customers Coming Soon

Returns a paginated list of all customers for your account, searchable by name or email.

In Development

Contact us for early access.


Sync API

The Trailing Paper Sync API enables bidirectional data exchange between your Trailing Paper account and external platforms — e-commerce stores, ERPs, marketplaces, and custom systems. Pull your TP data into your store, push your store's data back, or run a full bidirectional sync in a single call.

What you can sync

ResourceGET (pull from TP)POST (push to TP)Full Sync
Products / SKUs
Customers / Buyers
Vendors
Orders → Invoices
Purchase Orders
Packing Slips
Expenses
Ledger Entries
Credit Memos

Base URL

https://trailingpaper.com/api/sync

Upsert semantics

All POST endpoints use upsert logic. If a record with the same external_id or matching key field (SKU, email, etc.) already exists, it is updated. Otherwise a new record is created. Every response includes created, updated, and errors counts.


Sync API Authentication

The Sync API uses the same API key as every other Trailing Paper endpoint — no separate credentials. One key gives your external system access to payments, sync pull, and sync push.

Getting a key

Navigate to Settings → API Keys and generate a new key. Keys are automatically scoped to your active company, so the external system syncs data into the correct account.

Copy your key on creation. It is shown once in full. If you lose it, revoke it and generate a new one — existing integrations using the old key will stop working immediately.

Authenticating requests

Pass your key as a standard Bearer token on every request — the same header you use for the payment API:

Authorization: Bearer tp_live_your_key_here

What a key can do

Endpoint groupAccess granted
POST /api/chargeCreate a payment charge
GET /api/sync/*Pull any resource from Trailing Paper
POST /api/sync/*Push any resource into Trailing Paper
POST /api/sync/*/fullBidirectional sync (pull + push in one call)

Products

GET POST https://trailingpaper.com/api/sync/products Live

Pull all products from your Trailing Paper account, or push products from an external system into TP. Records are matched and upserted by SKU.

GET — Pull products from TP

Returns all active product records for your company, including SKU, name, price, stock quantity, category, and image URL.

Response

{
  "items": [
    {
      "id": 42,
      "sku": "SKU-1042",
      "name": "Classic Widget Pro",
      "description": "Heavy-duty all-purpose widget",
      "price": "49.99",
      "stock_quantity": 200,
      "category": "Hardware",
      "image_path": "https://trailingpaper.com/uploads/items/classic-widget-pro.jpg",
      "is_active": 1,
      "created_at": "2025-11-14T18:32:00Z"
    }
  ]
}

POST — Push products into TP

Send an array of product objects to create or update records. Matched by sku if it exists, otherwise by name.

Request body

FieldTypeRequiredDescription
itemsarrayYesArray of product objects
items[].skustringNoSKU — used as the upsert key
items[].namestringYesProduct name
items[].pricenumberNoUnit price
items[].stock_quantityintegerNoAvailable stock
items[].descriptionstringNoProduct description
items[].categorystringNoCategory name
items[].image_pathstringNoAbsolute image URL

Response

{
  "created": 3,
  "updated": 12,
  "deleted": 0,
  "errors": []
}

Customers

GET POST https://trailingpaper.com/api/sync/customers Live

Pull or push customer/buyer records. Records are matched by email when available, or by external_id.

GET — Pull customers

Returns all customers for your company including contact details and billing address.

POST — Push customers

Request body fields

FieldTypeRequiredDescription
items[].external_idstringNoYour platform's customer ID — used as the upsert key
items[].namestringYesFull name
items[].emailstringNoEmail address — used for deduplication
items[].phonestringNoPhone number
items[].addressstringNoFormatted billing address
items[].citystringNoCity
items[].statestringNoState / province
items[].postal_codestringNoZIP / postal code

Vendors

GET POST https://trailingpaper.com/api/sync/vendors Live

Pull or push vendor records. Matched by email or name.

POST — Push vendors

Request body fields

FieldTypeRequiredDescription
items[].namestringYesVendor / supplier name — upsert key
items[].emailstringNoContact email
items[].phonestringNoPhone number
items[].addressstringNoMailing address
items[].websitestringNoWebsite URL
items[].notesstringNoInternal notes

Orders

POST https://trailingpaper.com/api/sync/orders Live

Push orders from your store into Trailing Paper. Each order is converted into an invoice. Line items, customer lookup, and payment recording are all handled automatically.

POST — Push orders

Request body fields

FieldTypeRequiredDescription
items[].external_idstringYesYour store's order ID — upsert key; prevents duplicates
items[].buyer_idstringNoYour store's customer ID (used to resolve TP customer)
items[].buyer_emailstringNoCustomer email — fallback for customer lookup
items[].buyer_namestringNoCustomer name — created if no match found
items[].order_datestringNoISO 8601 date or datetime
items[].totalnumberNoOrder total in dollars
items[].statusstringNopending, processing, completed, cancelled, refunded
items[].payment_methodstringNoe.g. stripe, paypal, card
items[].line_itemsarrayNoArray of {sku, name, quantity, unit_price} objects
items[].shipping_addressobjectNo{address, city, state, postal_code, country}

Response

{
  "created": 2,
  "updated": 1,
  "errors": []
}

Completed orders with a total > 0 automatically generate a payment record and a matching ledger entry in Trailing Paper. Refunded orders trigger a reversal entry.


Purchase Orders

GET POST https://trailingpaper.com/api/sync/purchase-orders Live

Pull purchase orders from TP or push inbound purchase orders from your procurement system. Matched by source_po_id or po_number.

POST — Push purchase orders

Request body fields

FieldTypeRequiredDescription
items[].source_po_idstringNoYour system's PO ID — primary upsert key
items[].po_numberstringNoHuman-readable PO number — secondary upsert key
items[].vendor_namestringNoVendor name — resolved to a TP vendor record
items[].order_datestringNoISO 8601 date
items[].totalnumberNoPO total in dollars
items[].statusstringNodraft, sent, received, cancelled
items[].line_itemsarrayNoArray of {sku, name, quantity, unit_cost}

Expenses

GET POST https://trailingpaper.com/api/sync/expenses Live

Pull or push expense records. Useful for syncing platform fees, shipping costs, or ad spend from external sources into your TP financials.

POST — Push expenses

Request body fields

FieldTypeRequiredDescription
items[].expense_datestringYesISO 8601 date — part of upsert key
items[].amountnumberYesExpense amount in dollars — part of upsert key
items[].descriptionstringNoDescription or memo
items[].categorystringNoExpense category
items[].vendor_namestringNoVendor / payee name
items[].external_idstringNoYour system's expense ID — overrides date+amount key

Ledger Entries

GET POST https://trailingpaper.com/api/sync/ledger Live

Pull or push general ledger entries. Use this to sync accounting journal entries from an external ERP or accounting system.

POST — Push ledger entries

Request body fields

FieldTypeRequiredDescription
items[].entry_datestringYesISO 8601 date
items[].amountnumberYesEntry amount in dollars (positive = debit, negative = credit)
items[].descriptionstringNoEntry description / memo
items[].account_codestringNoChart of accounts code
items[].referencestringNoReference number or transaction ID
items[].external_idstringNoYour system's entry ID — primary upsert key when provided

Credit Memos

GET POST https://trailingpaper.com/api/sync/credit-memos Live

Pull or push credit memo records for returns, adjustments, or store credit.

POST — Push credit memos

Request body fields

FieldTypeRequiredDescription
items[].external_idstringNoYour system's memo ID — primary upsert key
items[].memo_numberstringNoHuman-readable memo number
items[].amountnumberNoCredit amount in dollars
items[].customer_namestringNoCustomer name — resolved to a TP customer record
items[].memo_datestringNoISO 8601 date
items[].reasonstringNoReason for the credit (e.g. return, adjustment)

Card Processing Config

GET https://trailingpaper.com/api/sync/finix-config Live

Discovery endpoint for downstream commerce sites that want to render the in-browser card form and route charges through your Trailing Paper merchant account. Returns the application id, environment, and availability flag your client-side script needs — so you don't have to copy-paste credentials onto every site.

Authenticate with the same X-TP-Sync-Key header used for the rest of the sync API. Cache the response for ~12 hours; invalidate when you rotate the sync key.

Request

GET /api/sync/finix-config HTTP/1.1
Host: trailingpaper.com
X-TP-Sync-Key: tp_live_xxxxxxxxxxxxxxxx
Accept: application/json

Response

{
  "success": true,
  "available": true,
  "application_id": "APij8YKTk1dcUwswCDdc2AtG",
  "env": "sandbox",
  "platform_merchant_available": true,
  "company_merchant_state": "APPROVED"
}

Response fields

FieldTypeDescription
availablebooleantrue when card processing is fully wired (API creds set, application id present, and a usable merchant id exists). Fall back to your manual-processing flow if false.
application_idstringPass to Finix.PaymentForm() client-side. Same id is shared across all sites under the same TP application.
envstringsandbox or live. Pass to Finix.PaymentForm().
platform_merchant_availablebooleanWhether the platform-level merchant id is configured. When true, sites without their own dedicated merchant can still accept cards.
company_merchant_statestringOnboarding state of this company's own Finix merchant (PROVISIONING, APPROVED, etc.) or null if not yet onboarded.

Charge Synced Order

POST https://trailingpaper.com/api/sync/charge-order Live

Charges a Trailing Paper invoice that was created via an earlier POST /api/sync/orders push. Designed for downstream checkouts that tokenize cards client-side via Finix.js and want the resulting transfer, payment record, and ledger entry to land on the synced TP invoice.

Authenticate with X-TP-Sync-Key. Idempotent — identical retries return the existing transfer rather than double-charging.

Request

POST /api/sync/charge-order HTTP/1.1
Host: trailingpaper.com
X-TP-Sync-Key: tp_live_xxxxxxxxxxxxxxxx
Content-Type: application/json

{
  "external_id": "315",
  "external_source": "redacted",
  "payment_instrument_id": "TKxxxxxxxxxxxxxxxx",
  "amount": 194.65
}

Request body fields

FieldTypeRequiredDescription
external_idstringYesYour store's order id, matching the external_id used in the original POST /api/sync/orders push.
external_sourcestringNoSource key matching the order push (defaults to redacted). TP also falls back to a direct id match if no source-scoped invoice is found.
payment_instrument_idstringYesFinix token. Accepts both TK… tokens (returned by Finix.js) and PI… payment instruments. TK tokens are exchanged to PI server-side before the transfer.
amountnumberNoAmount in dollars. Capped server-side to invoice.balance_due. Omit to charge the full balance. Minimum charge is $0.50.

Success response

{
  "success": true,
  "transfer_id": "TRxxxxxxxxxxxxxxxx",
  "state": "PENDING",
  "payment_id": 4421,
  "invoice_id": 1293,
  "amount": 194.65
}

Response fields

FieldTypeDescription
transfer_idstringFinix transfer id. Reflects in TP's payment record, ledger entry, and merchant dashboard.
statestringFinix transfer state (PENDING initially; SUCCEEDED on settlement; FAILED / CANCELED on rejection).
payment_idnumberTP payment row id.
invoice_idnumberTP invoice id this charge was applied to.
amountnumberFinal amount actually charged (after balance-due capping).

Failure responses

All failures return JSON with success: false and a human-readable error. Common codes:

HTTPMeaning
401Missing or invalid X-TP-Sync-Key.
404No invoice found for this external_id + external_source combination.
422Validation failure or Finix decline (card declined, balance already paid, amount below minimum, etc.). The error field carries the underlying message.
500Server-side error. Safe to retry — the idempotency key prevents double charges.

Idempotency: TP forms a Finix idempotency id of inv-<invoice_id>-<sha256_prefix> using the invoice id and the token. Retrying the exact same body returns the original transfer rather than a duplicate charge. Safe to retry on network blips.

Recommended call order

  1. Tokenize the card client-side via Finix.js (Finix.PaymentForm — see the integration playbook in /docs/cc-checkout-integration-playbook.md).
  2. Create the order in your store and push it to TP via POST /api/sync/orders. Wait for 200.
  3. Call POST /api/sync/charge-order with the token.
  4. On success: mark the order paid, fire fulfillment hooks, send the receipt email.
  5. On failure: leave the order unpaid and surface "card declined." Do not send a receipt email.

Create Invoice

POST https://trailingpaper.com/api/sync/invoice Live

Service-style invoice creator for downstream tools (e.g. the Papaya dashboard's rev-share billing) that need to bill arbitrary line items rather than e-commerce orders. Auto-creates the customer if it isn't already in your TP company. Idempotent on external_id.

Authenticate with X-TP-Sync-Key.

Request

POST /api/sync/invoice HTTP/1.1
Host: trailingpaper.com
X-TP-Sync-Key: tp_live_xxxxxxxxxxxxxxxx
Content-Type: application/json

{
  "customer": {
    "id": 1301,
    "external_source": "papaya-dashboard",
    "external_id": "site-9",
    "name": "Wheyk",
    "email": "billing@wheyk.com"
  },
  "items": [
    { "description": "Revenue share — April 2026", "quantity": 1, "unit_price": 1234.56 }
  ],
  "issue_date": "2026-05-01",
  "due_date":   "2026-05-15",
  "notes": "5% revenue share on $24,691.20 in net sales (Apr 1–30, 2026).",
  "payment_methods": ["zelle"],
  "external_source": "papaya-dashboard",
  "external_id": "rev-share-9-2026-04"
}

Customer matching priority

  1. customer.id — explicit pairing. If supplied, TP uses that customer record. Returns 422 if the id isn't in your company.
  2. customer.external_source + customer.external_id — for callers that maintain their own keying.
  3. customer.email — case-sensitive exact match.
  4. Case-insensitive exact match on customer.name within the same company.
  5. If no match, a new customer is created from the provided fields.

Payment method whitelist

Pass payment_methods as an array of method keys to restrict which options appear on the public invoice page. Accepted aliases: credit_card / card, bank / bank_transfer / ach / wire, zelle, venmo, cash. Omit the field to leave all methods enabled (the default allow_* toggles on the invoice).

Idempotency

If external_id is set and an invoice already exists with the same external_source + external_id in your company, TP returns the existing invoice (with "already_existed": true) instead of creating a duplicate. Safe to retry on network blips.

Success response

{
  "success": true,
  "already_existed": false,
  "invoice_id": 1305,
  "invoice_number": "INV-00043",
  "customer_id": 1301,
  "balance_due": 1234.56,
  "total": 1234.56,
  "status": "unpaid",
  "public_link": "https://trailingpaper.com/invoice/view/abc123…",
  "public_token": "abc123…",
  "external_source": "papaya-dashboard",
  "external_id": "rev-share-9-2026-04"
}

Send Invoice Email

POST https://trailingpaper.com/api/sync/invoice/{id}/send Live

Triggers the same email flow that the manual "Send Invoice" button uses on the TP UI — uses the company's email template, refreshes the public invoice link, stamps last_sent_at. Intended for downstream tools that previously created the invoice as a draft via POST /api/sync/invoice and want a separate review-then-send step.

Authenticate with X-TP-Sync-Key.

Request

POST /api/sync/invoice/1305/send HTTP/1.1
Host: trailingpaper.com
X-TP-Sync-Key: tp_live_xxxxxxxxxxxxxxxx
Content-Type: application/json

{
  "to": ["billing@wheyk.com"],
  "note": "Optional personal message rendered in the email body."
}

Request body fields

FieldTypeRequiredDescription
tostring or arrayNoOverride the recipient. Defaults to the customer's email on file.
notestringNoPersonal note to render above the invoice summary.

Success response

{
  "success": true,
  "invoice_id": 1305,
  "sent_to": ["billing@wheyk.com"],
  "sent_at": "2026-05-01 09:15:42"
}


Full Bidirectional Sync

Full sync endpoints combine a GET (pull from your store) and a POST (push from TP) in a single atomic call. Use these for scheduled nightly syncs or on-demand reconciliation.

EndpointWhat it does
POST /api/sync/products/fullPulls products from your store URL, upserts into TP, then pushes all TP products back to your store
POST /api/sync/vendors/fullPulls vendors from your store, upserts into TP, pushes TP vendors back
POST /api/sync/purchase-orders/fullBidirectional PO sync
POST /api/sync/expenses/fullBidirectional expense sync
POST /api/sync/ledger/fullBidirectional ledger sync
POST /api/sync/credit-memos/fullBidirectional credit memo sync

Full sync endpoints require your External Store Base URL to be configured in Settings → Company APIs. They authenticate to your store using your Sync API credentials.

Full sync response

{
  "pulled": {
    "created": 8,
    "updated": 34,
    "deleted": 0,
    "errors": []
  },
  "pushed": 42
}

Triggering from the dashboard

You can also trigger full syncs from the Settings → Company APIs page using the sync buttons — no code required.


Webhook Events

Trailing Paper sends webhook notifications to your registered HTTPS endpoint when events occur on your account. Webhooks are the recommended way to track async state changes — particularly transfer.succeeded and transfer.failed, since charges start as PENDING and finalize asynchronously.

Webhook registration is coming soon. You will be able to register your endpoint URL from the dashboard. Until then, poll the retrieve endpoint or use the merchant dashboard to monitor charge status.

Event object structure

FieldTypeDescription
eventstringEvent type, e.g. transfer.succeeded.
created_atstringISO 8601 timestamp when the event occurred.
dataobjectThe resource that triggered the event. Shape varies by event type.

Event types

EventDescription
transfer.succeededA charge was authorized and will settle on the next payout date.
transfer.failedA charge was declined or failed. The data.failure_code field describes the reason.
invoice.paidAn invoice was paid in full.
invoice.overdueAn invoice passed its due date without payment.
payout.sentA bank payout was initiated to your linked account.
dispute.createdA customer filed a chargeback. You have a limited window to respond.

Best practices

  • Respond with 200 OK within 5 seconds. Do heavy processing asynchronously.
  • Use the transfer_id in the event payload to look up the charge in your system.
  • Make your handler idempotent — the same event may be delivered more than once.
  • Always verify the webhook signature (see below) before processing.

Verifying Signatures

Every webhook delivery will include a X-TP-Signature header containing an HMAC-SHA256 signature of the raw request body using your webhook secret. Verifying this signature is critical — it proves the request originated from Trailing Paper and not a third party.

Coming with Webhook Registration

Full signature verification documentation will be published when webhook delivery launches. The verification algorithm will follow the industry-standard HMAC-SHA256 pattern used by Stripe and Finix.


Object Reference

Transfer

Returned by POST /api/charge. Represents a single card payment transaction.

FieldTypeDescription
successbooleanAlways true on success.
transfer_idstringUnique ID. Prefix: TR. Use for dashboard lookups and webhook matching.
status string PENDING — created, awaiting finalization
SUCCEEDED — authorized, will settle
FAILED — declined or errored
amountintegerAmount in cents.
currencystringISO 4217 currency code. Currently always USD.

Changelog

April 24, 2026 — v1.0

  • New: POST /api/charge — create a card charge via Finix Payment Instrument ID
  • New: API key management at /api-keys — generate, label, and revoke keys
  • New: Bearer token authentication (tp_live_-prefixed 56-character keys)
  • New: Idempotency-Key header support on POST endpoints
  • New: This developer documentation
30-second quickstart 1. Generate an API key at /api-keys
2. Tokenize a card with Finix.js
3. POST to /api/charge with the instrument ID
4. Handle the webhook when status changes
cURL PHP Node.js
# Include this header on every request
curl https://trailingpaper.com/api/charge \
  -H "Authorization: Bearer tp_live_your_key_here" \
  ...
// Helper function for authenticated requests
function tp_request($method, $path, $body = null): array {
  $ch = curl_init('https://trailingpaper.com/api' . $path);
  $headers = [
    'Authorization: Bearer ' . getenv('TP_API_KEY'),
    'Content-Type: application/json',
  ];
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => $method,
    CURLOPT_HTTPHEADER     => $headers,
    CURLOPT_POSTFIELDS     => $body ? json_encode($body) : null,
  ]);
  $res = json_decode(curl_exec($ch), true);
  curl_close($ch);
  return $res;
}
// Base fetch wrapper
const tp = async (method, path, body) => {
  const res = await fetch(
    `https://trailingpaper.com/api${path}`,
    {
      method,
      headers: {
        'Authorization': `Bearer ${process.env.TP_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: body ? JSON.stringify(body) : undefined,
    }
  );
  return res.json();
};
cURL
# Live (production) — active now
BASE="https://trailingpaper.com/api"
curl -X POST "$BASE/charge" \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" \
  -d '{"amount": 9999, ...}'

# Sandbox — coming soon
BASE="https://sandbox.trailingpaper.com/api"
cURL PHP
curl -X POST https://trailingpaper.com/api/charge \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716" \
  -d '{
    "amount": 9999,
    "payment_instrument_id": "PI...",
    "description": "Order #1234"
  }'
// Required headers for every POST
$headers = [
  'Authorization: Bearer tp_live_...',
  'Content-Type: application/json',
  'Idempotency-Key: order_' . $orderId,
];
cURL PHP
# First attempt
curl -X POST https://trailingpaper.com/api/charge \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_8742" \
  -d '{"amount": 4999, "payment_instrument_id": "PI..."}'

# Retry with same key — no duplicate charge
curl -X POST https://trailingpaper.com/api/charge \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_8742" \
  -d '{"amount": 4999, "payment_instrument_id": "PI..."}'
// Tie the key to your order, not the attempt
$key = 'order_' . $order['id'];

for ($try = 0; $try < 3; $try++) {
  $resp = tp_charge($amount, $instrument, $key);
  if (!isset($resp['error'])) break;
  sleep(2 ** $try); // 1s, 2s, 4s
}
Responses
# 201 — success
{ "success": true, "transfer_id": "TRxx..." }

# 400 — validation error (field-level)
{
  "error": {
    "amount": "The amount field is required."
  }
}

# 401 — bad or missing key
{ "error": "Invalid or inactive API key." }

# 422 — card declined
{ "error": "Payment processing failed." }

# 422 — merchant not approved
{ "error": "Merchant account not approved for payments." }

# 429 — rate limited
HTTP/2 429
Retry-After: 12
{ "error": "Rate limit exceeded." }
Backoff — PHP
function charge_with_backoff($params): array
{
  $delay = 1;
  for ($i = 0; $i < 4; $i++) {
    [$resp, $code] = tp_post('/charge', $params);
    if ($code !== 429) return $resp;
    sleep($delay);
    $delay = min($delay * 2, 30);
  }
  throw new \RuntimeException('Rate limit exceeded after retries');
}
cURL PHP Node.js Python
curl -X POST https://trailingpaper.com/api/charge \
  -H "Authorization: Bearer tp_live_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_8742" \
  -d '{
    "amount": 9999,
    "payment_instrument_id": "PIxxxxxxxxxxxxxxxxxxxxxxxxx",
    "description": "Order #1234"
  }'
$payload = [
  'amount'                => 9999,
  'payment_instrument_id' => 'PIxxxxxxxxxxxxxxxxxx',
  'description'           => 'Order #1234',
];

$ch = curl_init('https://trailingpaper.com/api/charge');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST           => true,
  CURLOPT_HTTPHEADER     => [
    'Authorization: Bearer ' . getenv('TP_API_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: order_' . $orderId,
  ],
  CURLOPT_POSTFIELDS     => json_encode($payload),
]);

$response = json_decode(curl_exec($ch), true);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode === 201) {
  echo "Transfer: " . $response['transfer_id'];
  echo "Status: "   . $response['status'];    // PENDING
} else {
  error_log("Charge failed: " . $response['error']);
}
const response = await fetch(
  'https://trailingpaper.com/api/charge',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.TP_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `order_${orderId}`,
    },
    body: JSON.stringify({
      amount: 9999,
      payment_instrument_id: 'PIxxxxxxxxxxxxxxxxxx',
      description: 'Order #1234',
    }),
  }
);

if (!response.ok) {
  const err = await response.json();
  throw new Error(err.error);
}

const data = await response.json();
console.log(data.transfer_id); // TRxxxxxxxxxxxxxxx
console.log(data.status);      // PENDING
import os, requests

resp = requests.post(
    'https://trailingpaper.com/api/charge',
    headers={
        'Authorization': f'Bearer {os.environ["TP_API_KEY"]}',
        'Content-Type': 'application/json',
        'Idempotency-Key': f'order_{order_id}',
    },
    json={
        'amount': 9999,
        'payment_instrument_id': 'PIxxxxxxxxxxxxxxxxxx',
        'description': 'Order #1234',
    },
    timeout=10,
)
resp.raise_for_status()
data = resp.json()
print(data['transfer_id'])  # TRxxxxxxxxxxxxxxx
print(data['status'])       # PENDING
201 — Created
{
  "success": true,
  "transfer_id": "TRe3cca196614311ef9a0086369df7a9a",
  "status": "PENDING",
  "amount": 9999,
  "currency": "USD"
}
Coming SoonThis endpoint is in development.
Coming SoonThis endpoint is in development.
Use Finix.js today Finix's client-side SDK tokenizes cards in the browser and returns a payment_instrument_id accepted by POST /api/charge right now.
Coming SoonInvoice API is in development.
Coming SoonInvoice API is in development.
Coming SoonInvoice API is in development.
Coming SoonCustomers API is in development.
Coming SoonCustomers API is in development.
Endpoints
# All Sync API base paths
GET/POST  /api/sync/products
GET/POST  /api/sync/customers
GET/POST  /api/sync/vendors
POST      /api/sync/orders
GET/POST  /api/sync/purchase-orders
GET/POST  /api/sync/expenses
GET/POST  /api/sync/ledger
GET/POST  /api/sync/credit-memos
# Full bidirectional sync
POST      /api/sync/products/full
POST      /api/sync/vendors/full
POST      /api/sync/expenses/full
POST      /api/sync/ledger/full
curl PHP Node.js Python
# Same key for every TP endpoint
curl https://trailingpaper.com/api/sync/products \
  -H "Authorization: Bearer tp_live_your_key"
$key = getenv('TP_API_KEY');
$client = Services::curlrequest();
$response = $client->get('https://trailingpaper.com/api/sync/products', [
  'headers' => [
    'Authorization' => 'Bearer ' . $key,
  ],
]);
const res = await fetch('https://trailingpaper.com/api/sync/products', {
  headers: {
    'Authorization': 'Bearer ' + process.env.TP_API_KEY,
  }
});
import requests, os
r = requests.get(
  'https://trailingpaper.com/api/sync/products',
  headers={'Authorization': 'Bearer ' + os.environ['TP_API_KEY']}
)
GET POST
curl https://trailingpaper.com/api/sync/products \
  -H "Authorization: Bearer tp_live_..."

# Response
{
  "items": [{
    "id": 42,
    "sku": "SKU-1042",
    "name": "Classic Widget Pro",
    "price": "49.99",
    "stock_quantity": 200
  }]
}
curl -X POST https://trailingpaper.com/api/sync/products \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" \
  -d '{"items":[{
    "sku":"SKU-1042",
    "name":"Classic Widget Pro",
    "price":49.99,
    "stock_quantity":200
  }]}'

# Response
{ "created":1, "updated":0, "deleted":0, "errors":[] }
POST
curl -X POST https://trailingpaper.com/api/sync/customers \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" \
  -d '{"items":[{
    "external_id":"wc_cust_8821",
    "name":"Jordan Lee",
    "email":"jordan@example.com",
    "city":"Austin","state":"TX"
  }]}'
POST
curl -X POST https://trailingpaper.com/api/sync/vendors \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" \
  -d '{"items":[{
    "name":"Acme Supplies Inc.",
    "email":"orders@acmesupplies.example.com"
  }]}'
POST
curl -X POST https://trailingpaper.com/api/sync/orders \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" \
  -d '{"items":[{
    "external_id":"wc_order_10042",
    "buyer_email":"jordan@example.com",
    "buyer_name":"Jordan Lee",
    "order_date":"2026-04-22",
    "total":149.97,
    "status":"completed",
    "payment_method":"stripe",
    "line_items":[
      {"sku":"SKU-1042","name":"Classic Widget Pro",
       "quantity":3,"unit_price":49.99}
    ]
  }]}'

# Response
{ "created":1, "updated":0, "errors":[] }
POST
curl -X POST https://trailingpaper.com/api/sync/purchase-orders \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" \
  -d '{"items":[{
    "source_po_id":"PO-2026-004",
    "vendor_name":"Acme Supplies Inc.",
    "order_date":"2026-04-20",
    "total":820.00,
    "status":"received"
  }]}'
POST
curl -X POST https://trailingpaper.com/api/sync/expenses \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" \
  -d '{"items":[{
    "expense_date":"2026-04-22",
    "amount":42.00,
    "description":"Stripe processing fees",
    "category":"Payment Processing",
    "external_id":"stripe_fee_apr22"
  }]}'
POST
curl -X POST https://trailingpaper.com/api/sync/ledger \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" \
  -d '{"items":[{
    "entry_date":"2026-04-22",
    "amount":149.97,
    "description":"WC Order #10042",
    "account_code":"4000",
    "external_id":"wc_ledger_10042"
  }]}'
POST
curl -X POST https://trailingpaper.com/api/sync/credit-memos \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" \
  -d '{"items":[{
    "external_id":"wc_refund_8821",
    "amount":49.99,
    "customer_name":"Jordan Lee",
    "memo_date":"2026-04-23",
    "reason":"return"
  }]}'
curl Node.js scheduler
# Full bidirectional product sync
curl -X POST https://trailingpaper.com/api/sync/products/full \
  -H "Authorization: Bearer tp_live_..."

# Response
{
  "pulled": { "created":8, "updated":34, "errors":[] },
  "pushed": 42
}
const SYNCS = ['products','vendors','expenses','ledger'];
for (const ep of SYNCS) {
  const r = await fetch(
    `https://trailingpaper.com/api/sync/${ep}/full`,
    {
      method:'POST',
      headers:{
        'Authorization': 'Bearer ' + process.env.TP_API_KEY,
      }
    }
  );
  const d = await r.json();
  console.log(`${ep} done`, d);
}
Payload
# transfer.succeeded event
{
  "event":      "transfer.succeeded",
  "created_at": "2026-04-24T14:22:11Z",
  "data": {
    "transfer_id": "TRxxxxxxxxxxxxxxxxx",
    "amount":      9999,
    "currency":   "USD",
    "status":     "SUCCEEDED",
    "description": "Order #1234"
  }
}

# transfer.failed event
{
  "event": "transfer.failed",
  "data": {
    "transfer_id":  "TRxxxxxxxxxxxxxxxxx",
    "failure_code": "INSUFFICIENT_FUNDS",
    "status":       "FAILED"
  }
}
Coming with Webhook RegistrationHMAC-SHA256 signature verification docs will be published at launch.
Transfer object
{
  "success":     true,
  "transfer_id": "TRe3cca196614311ef9a0086",
  "status":      "PENDING",  // →SUCCEEDED | FAILED
  "amount":      9999,       // cents
  "currency":   "USD"
}
v1.0 — April 24, 2026Initial release of the Trailing Paper API with charge endpoint, API key management, and idempotency support.