Tpay
DOCS

Recurring Payments

Recurring payments enable automatic collection of funds from the Payer at defined intervals, without requiring the user to initiate each payment manually.

The recurring engine supports:

  • Card payments (tokenization)
  • BLIK recurring payments

What is the recurring engine?

The recurring engine is a solution that enables automated management of recurring payments. It automatically charges payments according to a defined schedule, retries failed attempts to increase collection success, and sends notifications about payment status.

It is particularly useful for:

  • subscriptions (e.g. VOD, SaaS, fitness)
  • recurring billing (utilities, memberships)
  • installment payments

Before start

How does the recurring engine work?

  1. The Merchant creates a recurring payment
  2. The system stores the schedule
  3. A payment attempt is executed at the scheduled time
  4. If a payment fails, the system retries the charge according to the default retry policy or a custom retry schedule configured by the Merchant
  5. The Merchant receives a webhook with the result
Note

By default, the system retries failed charges once per day for up to 14 days.

A custom retry schedule can be configured using the schedule.retryIntervals field when creating a recurring payment.

Responsibility split

The recurring engine provides backend functionality (API).
The Merchant is responsible for the UI and business logic.

Merchant responsibilities:

  • obtaining a token/alias (e.g. via the first payment)
  • initializing recurring payments
  • handling notifications
  • managing subscriptions (cancellation or payment method updates)

Tpay responsibilities:

  • processing payments
  • retrying failed attempts
  • managing the payment schedule

Recurring identifier (ULID)

The id field must be:

  • unique
  • generated on the Merchant side
  • in ULID format

Example:

01K1Z6T40ZWKY1XZG9VD5AZ9Q4
Note

We recommend generating and storing ULIDs on the Merchant side to avoid conflicts.

Dictionaries

Recurring status

ValueDescription
active
Recurring is active and will continue executing charges
finished
Recurring has ended (charge limit reached)
failed
Recurring has ended due to failure
canceled
Recurring was canceled by the Merchant

Payment status

ValueDescription
pending
Payment is being processed
correct
Payment completed successfully
failed
Payment failed
canceled
Payment was canceled

Failure reasons

ValueDescription
insufficient funds
Insufficient funds on the payer's account
instrument invalid
Payment instrument is invalid or expired
instrument rejected
Payment instrument was rejected by the provider
payer rejected
Payment was rejected by the payer
internal error
Internal system error occurred

Create recurring payment

To create a recurring payment, send a POST request to:

https://api.tpay.com/recurring

See details in API Reference: POST /recurring

Provide the following parameters in the request:

id
 *
unique ULID identifying the recurring payment
description
 *
transaction description visible to the payer
hiddenDescription
technical description not visible to the payer
payer.email
 *
payer’s email address
payer.name
 *
payer’s full name
payer.phone
phone number (with country code)
payer.address
street and building number
payer.code
postal/zip code
payer.city
city
payer.country
country
payer.taxId
tax identification number
schedule.amount
 *
single charge amount
schedule.currency
 *
currency (PLN)
schedule.firstChargeDate
 *
date of the first charge
schedule.interval
 *
number of days/months between charges
schedule.intervalType
 *
days or months
schedule.chargeCount
number of charges to be performed. Passing null means charges will continue until canceled
schedule.retryIntervals
Optional custom retry schedule for failed charges. Each interval defines the delay before the next retry attempt. If provided, it overrides the default retry policy.
paymentInstrument.paymentType
 *
card_token or blik_payid
paymentInstrument.value
 *
card token / BLIK alias
paymentInstrument.blik.model
BLIK model (A, M, O)
paymentInstrument.blik.noDelay
charge without delay (only for A and O models)
callbackUrl
 *
endpoint that will receive notifications

* Required fields

Request body example

{
  "id": "01K1Z6T40ZWKY1XZG9VD5AZ9Q4",
  "description": "Recurring Order AB-CD-12",
  "hiddenDescription": "1234-ABC-89",
  "payer": {
    "email": "[email protected]",
    "name": "Jan Kowalski",
    "phone": "00123456",
    "address": "ul. Przykładowa 44b/2",
    "code": "00-001",
    "city": "Warszawa",
    "country": "PL",
    "taxId": "PL3774716081"
  },
  "schedule": {
    "amount": 12.34,
    "currency": "PLN",
    "firstChargeDate": "2025-11-12T12:34:00+02:00",
    "interval": 1,
    "intervalType": "months",
    "chargeCount": 12
  },
  "paymentInstrument": {
    "paymentType": "blik_payid",
    "value": "alias_123",
    "blik": {
      "model": "A",
      "noDelay": true
    }
  },
  "callbackUrl": "https://your-callback-url"
}

Example:

curl --location 'https://api.tpay.com/recurring' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <access_token>' \
--data-raw '{
  "id": "01K1Z6T40ZWKY1XZG9VD5AZ9Q4",
  "description": "Recurring Order AB-CD-12",
  "hiddenDescription": "1234-ABC-89",
  "payer": {
    "email": "[email protected]",
    "name": "Jan Kowalski",
    "phone": "00123456",
    "address": "ul. Przykładowa 44b/2",
    "code": "00-001",
    "city": "Warszawa",
    "country": "PL",
    "taxId": "PL3774716081"
  },
  "schedule": {
    "amount": 12.34,
    "currency": "PLN",
    "firstChargeDate": "2025-11-12T12:34:00+02:00",
    "interval": 1,
    "intervalType": "months",
    "chargeCount": 12
  },
  "paymentInstrument": {
    "paymentType": "card_token",
    "value": "token_123"
  },
  "callbackUrl": "https://your-callback-url"
}'

Example response:

{
  "id": "01K1Z6T40ZWKY1XZG9VD5AZ9Q4"
}

The recurring creation endpoint may return different statuses depending on the situation:

HTTP StatusDescription
201 Created
Recurring payment was successfully created
200 OK
Recurring with the given ID already exists and data matches (idempotent)
409 Conflict
Recurring with the given ID already exists but data differs
400 Bad Request
Invalid input data (validation error)
401 Unauthorized
Invalid authorization credentials
404 Not Found
Recurring exists but does not belong to the specified Merchant
500 Internal Server Error
Server error
Note

Recurring creation is an idempotent operation.

Scenario:

  • first request → 201 Created
  • retrying the same request → 200 OK
  • same ID with different data → 409 Conflict

This allows you to safely retry requests (e.g. after a timeout).

Custom Retry Schedule

By default, the recurring engine uses a retry policy that retries failed charges once per day for up to 14 days.

A Merchant may define a custom retry schedule for a specific recurring payment by providing the schedule.retryIntervals field.

Each item in the array defines the delay before the next retry attempt after a failed charge.

Example:

"retryIntervals": [
  {
    "value": 10,
    "unit": "minutes"
  },
  {
    "value": 12,
    "unit": "hours"
  },
  {
    "value": 1,
    "unit": "days"
  }
]

The above configuration means:

  • first retry after 10 minutes
  • second retry after 12 hours
  • third retry after 1 day

Once all configured retry attempts have been exhausted, no further retry attempts will be scheduled.

If the retryIntervals field is not provided, the default retry policy will be applied.

Note

The maximum number of configured retry attempts is 30.

Fetch recurring payment

To retrieve recurring payment details, send a GET request to:

https://api.tpay.com/recurring/{recurringId}

Replace {recurringId} with your recurring identifier:

https://api.tpay.com/recurring/01K1Z6T40ZWKY1XZG9VD5AZ9Q4

See details in API Reference: GET /recurring/{recurringId}

Example request

curl --location 'https://api.tpay.com/recurring/01K1Z6T40ZWKY1XZG9VD5AZ9Q4' \
--header 'Authorization: Bearer <access_token>'

Example response

{
  "id": "01K1Z6T40ZWKY1XZG9VD5AZ9Q4",
  "description": "Recurring Order AB-CD-12",
  "hiddenDescription": "1234-ABC-89",
  "status": "active",
  "payer": {
    "email": "[email protected]",
    "name": "Jan Kowalski"
  },
  "schedule": {
    "amount": 12.34,
    "currency": "PLN",
    "interval": 1,
    "intervalType": "months",
    "chargeCount": 12
  },
  "paymentInstrument": {
    "paymentType": "card_token"
  },
  "callbackUrl": "https://your-callback-url",
  "reason": "insufficient funds"
}
Note

The reason field is returned only if the last payment attempt failed.

Fetch recurring transactions

To retrieve a list of all payment attempts (transactions) for a given recurring payment, send a GET request to:

https://api.tpay.com/recurring/{recurringId}/transactions

Replace {recurringId} with your recurring identifier:

https://api.tpay.com/recurring/01K1Z6T40ZWKY1XZG9VD5AZ9Q4/transactions

See details in API Reference: GET /recurring/{recurringId}/transactions

Example request

curl --location 'https://api.tpay.com/recurring/01K1Z6T40ZWKY1XZG9VD5AZ9Q4/transactions' \
--header 'Authorization: Bearer <access_token>' \
--header 'X-Merchant-Id: 01HXXX'

Example response

{
  "items": [
    {
      "transactionId": "01KC92SARJB28QM9SCHBXFRENH",
      "createdAt": "2025-11-12T12:34:00+02:00",
      "status": "correct",
      "iterationCount": 1,
      "iterationAttemptCount": 1,
      "reason": null
    },
    {
      "transactionId": "01KC93C3XD55XV39JQAQQY20R4",
      "createdAt": "2025-12-12T12:34:00+02:00",
      "status": "failed",
      "iterationCount": 2,
      "iterationAttemptCount": 1,
      "reason": "insufficient funds"
    },
    {
      "transactionId": "01KC93C8PDVNK9F9NTW9KPPQTP",
      "createdAt": "2025-12-13T12:34:00+02:00",
      "status": "pending",
      "iterationCount": 2,
      "iterationAttemptCount": 2,
      "reason": null
    }
  ]
}
FieldDescription
transactionId
Identifier of a single transaction in Tpay.
createdAt
Date and time when the payment attempt was created.
status
Status of the payment attempt.
iterationCount
Sequential number of the charge within the schedule.
iterationAttemptCount
Attempt number within a given charge iteration.
reason
Reason for failure (if applicable).

Each payment (iteration) can have multiple attempts (iterationAttempt).

Example:

Iteration 2:

iterationAttempt 1 → FAILED
iterationAttempt 2 → FAILED
iterationAttempt 3 → SUCCESS

Retry recurring payment

This endpoint allows you to immediately retry the last failed payment attempt for a recurring payment.

Retry enables forcing another payment attempt outside of the standard schedule.

It can be used in scenarios such as:

  • the customer has added funds
  • the customer requested another payment attempt
  • the Merchant wants to speed up the retry process

To retry the last failed attempt, send a POST request to:

https://api.tpay.com/recurring/{recurringId}/retry

Replace {recurringId} with your recurring identifier:

https://api.tpay.com/recurring/01K1Z6T40ZWKY1XZG9VD5AZ9Q4/retry

See details in API Reference: POST /recurring/{recurringId}/retry

Execution conditions

Retry will be performed only if:

  • the recurring status is active
  • the last attempt status is failed
  • the maximum number of retry attempts has not been exceeded

Example

curl --location --request POST 'https://api.tpay.com/recurring/01F8MECHZX3TBDSZ7XRADM79XV/retry' \
--header 'Authorization: Bearer <access_token>' \
--header 'X-Merchant-Id: 01HXXX'

Example response

{
  "id": "01F8MECHZX3TBDSZ7XRADM79XV"
}

Response statuses

  • 201 – retry has been successfully scheduled
  • 404 – recurring not found or access denied
  • 406 – recurring is not active or the last attempt was not failed
Note
  • retry creates a new payment attempt
  • retry is processed asynchronously
  • retry does not modify the schedule
  • retry does not affect future billing cycles

Cancel recurring payment

This endpoint allows you to cancel an active recurring payment.

When to use?

Use this endpoint when:

  • the customer has canceled the service
  • the subscription has expired
  • you want to stop further charges

After cancellation:

  • no further payments will be executed
  • retry attempts are stopped
  • the schedule is no longer active

To cancel a recurring payment, send a POST request to:

https://api.tpay.com/recurring/{recurringId}/cancel

Replace {recurringId} with your recurring identifier:

https://api.tpay.com/recurring/01K1Z6T40ZWKY1XZG9VD5AZ9Q4/cancel

See details in API Reference: POST /recurring/{recurringId}/cancel

Example

curl --location --request POST 'https://api.tpay.com/recurring/01K1Z6T40ZWKY1XZG9VD5AZ9Q4/cancel' \
--header 'Authorization: Bearer <access_token>' \
--header 'X-Merchant-Id: 01HXXX'

Example response

{
  "id": "01K1Z6T40ZWKY1XZG9VD5AZ9Q4"
}

Response statuses

  • 200 – recurring has been successfully canceled
  • 401 – unauthorized
  • 404 – recurring not found or access denied
  • 500 – internal server error
Note
  • cancellation stops all future payments
  • retry for the current payment is stopped
  • cancellation does not revert already completed payments

Update payment instrument

This endpoint allows you to update the payment instrument for an existing recurring payment.

It can be used to:

  • replace a card
  • change a BLIK alias
  • restore payments after a rejection

To update the payment method, send a POST request to:

https://api.tpay.com/recurring/{recurringId}/payment_instrument

Replace {recurringId} with your recurring identifier:

https://api.tpay.com/recurring/01K1Z6T40ZWKY1XZG9VD5AZ9Q4/payment_instrument

Specify the following parameters in the request:

paymentInstrument.paymentType
 *
card_token or blik_payid
paymentInstrument.value
 *
card token / BLIK alias
paymentInstrument.blik.model
BLIK model (A, M, O)
paymentInstrument.blik.noDelay
charge without delay (only for models A and O)

* Required fields.

The basic request body should look like this:

{
  "paymentInstrument": {
    "paymentType": "card_token",
    "value": "token_123456"
  }
}

Example

curl --location --request POST 'https://api.tpay.com/recurring/01K1Z6T40ZWKY1XZG9VD5AZ9Q4/payment_instrument' \
--header 'Authorization: Bearer <access_token>' \
--header 'X-Merchant-Id: 01HXXX' \
--header 'Content-Type: application/json' \
--data-raw '{
  "paymentInstrument": {
    "paymentType": "card_token",
    "value": "token_123456",
  }
}'

Example response

{
  "id": "01K1Z6T40ZWKY1XZG9VD5AZ9Q4"
}

Response statuses

  • 201 – payment instrument successfully updated
  • 400 – invalid request data
  • 401 – unauthorized
  • 404 – recurring not found
  • 500 – internal server error
Note
  • the change applies to future payments
  • retry will use the updated instrument
  • the operation is processed asynchronously

Notification

After each payment attempt, the system sends a notification (webhook) to the callbackUrl defined by the Merchant.

The notification contains details about the payment attempt.

Notifications are sent:

  • after each payment attempt
  • regardless of the result (success / failed)

Notifications allow you to:

  • update subscription status on the Merchant side
  • inform users about payment issues
  • react to successful payments (e.g. activate a service)

Notification structure:

{
  "recurringId": "01K1Z6T40ZWKY1XZG9VD5AZ9Q4",
  "transactionId": "01KC92SARJB28QM9SCHBXFRENH",
  "hiddenDescription": "1234-ABC-89",
  "iterationCount": 2,
  "iterationAttemptCount": 1,
  "status": "failed",
  "nextChargeDate": "2025-12-13T12:34:00+02:00",
  "reason": "insufficient funds"
}
FieldDescription
recurringId
Recurring payment identifier.
transactionId
Identifier of a single transaction (attempt).
hiddenDescription
Technical description provided during recurring creation.
iterationCount
Sequential number of the charge within the schedule.
iterationAttemptCount
Attempt number for a given charge.
status
Payment status.
nextChargeDate
Date of the next payment attempt. May be null if no further attempts are scheduled (recurring finished).
reason
Reason for failure (if applicable).

How to respond to a notification?

To confirm successful receipt of a notification, return:

HTTP Code: 200

Body:

{
  "result": true
}
Note

Notifications may be sent multiple times.

If the message is not received or the response content is different, we will retry according to the scheme.