Coingate

Accept crypto with CoinGate

Accept crypto with confidence using everything you need in one platform.

Tutorials

Building Recurring Crypto Billing With the Billing API

This guide builds working crypto subscription payments end to end.
Building Recurring Crypto Billing With the Billing API
Last updated: August 6, 2026 8 min read
VB
Vilius Barbaravičius

Subscriptions are easy with cards. You store the card, you charge it every month, the customer does nothing. Crypto does not work that way, and pretending otherwise is how integrations go wrong.

A blockchain payment is push, not pull. You cannot silently reach into a customer’s wallet and take this month’s fee. Every payment needs the wallet holder to sign it. So “recurring crypto billing” means something specific: on a schedule, you generate and send an invoice, and the customer pays it. The Billing API automates the generating-and-sending part, which is the part that actually eats your time.

This guide builds working crypto subscription payments end to end. If you want the business case first, our writeup on crypto payments and involuntary SaaS churn covers why failed cards are worth designing around. Here we are building. If you are new to the API, get oriented with our crypto payment API integration guide first.


Raising the same invoices by hand every month? Put them on a schedule instead.


Standard auth and base URL, with sandbox at https://api-sandbox.coingate.com/v2:

Authorization: Token YOUR_API_TOKEN

The object model, in the order you will build it

Three objects, and they stack:

  • A billing contact is the customer. You need one before anything else.
  • A billing product is an optional reusable line item with a fixed price. Skip it if you would rather pass a raw amount.
  • A recurring billing is the schedule. It references the contact and, every interval, automatically creates and sends a billing request (an invoice with a pay link) to that customer.

So the mental model is: set up who you are billing, optionally what for, then the cadence. Let us go in that order.

Step one: create the contact

curl --request POST "https://api.coingate.com/v2/billing/contacts" 
  -H "Authorization: Token YOUR_API_TOKEN" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  --data-urlencode "contact_type=person" 
  --data-urlencode "email=joe@doe.com" 
  --data-urlencode "first_name=Joe" 
  --data-urlencode "surname=Doe" 
  --data-urlencode "external_contact_id=user_AA1234"

The external_contact_id is your own user ID. Set it. It is what lets you tie a CoinGate contact back to the user in your database later without keeping a separate mapping table. For a business contact, set contact_type=business and pass company_name instead of the name fields.

You get back an id. Hold onto it.

{
  "id": 1,
  "contact_type": "person",
  "email": "joe@doe.com",
  "external_contact_id": "user_AA1234",
  "created_at": "2026-08-06T15:38:35.787Z"
}

Step two, optional: create a product

If you are billing the same thing repeatedly, a product saves you from repeating the amount everywhere:

curl --request POST "https://api.coingate.com/v2/billing/products" 
  -H "Authorization: Token YOUR_API_TOKEN" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  --data-urlencode "name=Pro Plan" 
  --data-urlencode "price=49.0" 
  --data-urlencode "currency_id=11"

Products are tied to a currency, so a “Pro Plan” priced in euro is its own product. The currency_id is the numeric ID from the currencies endpoint, not the symbol. Look it up rather than guessing: GET /v2/currencies?symbol=EUR returns "id": 11. You can skip products entirely and just pass an amount on the schedule instead. Your call.

Step three: create the recurring schedule

This is the one that does the work:

curl --request POST "https://api.coingate.com/v2/billing/recurrings" 
  -H "Authorization: Token YOUR_API_TOKEN" 
  -H "Content-Type: application/json" 
  -d '{
    "billing_contact_id": 1,
    "currency_id": 11,
    "receive_currency_id": 11,
    "frequency": "monthly",
    "start_at": "2026-09-01",
    "amount": "49.0",
    "title": "Pro Plan subscription",
    "callback_url": "https://yourapp.com/coingate/billing"
  }'

The important fields:

  • frequency is either weekly or monthly. Those are your two cadences.
  • start_at is the date the first invoice goes out, and it is also the anchor day for every invoice after it. Start on the first, get billed on the first. If the anchor day does not exist in a later month, it resolves to the last valid day, so a schedule starting 31 January bills on 28 February.
  • currency_id is what you price in, receive_currency_id is what you settle in. Price in euro and settle in euro, or price in euro and settle in USDC. Two separate decisions.
  • amount is required only if you did not attach a product. If you did, pass billing_request_items with the product ID and quantity instead.
  • end_at is optional. Leave it out and the subscription runs until you stop it.
  • underpaid_cover_pct is optional too, and it is the quiet useful one. It sets how much of a shortfall you will absorb, 0 to 10 percent, default 0, which matters when a customer’s wallet fee eats into the amount that lands.

The response confirms the schedule and, importantly, tells you when the next invoice goes out:

{
  "id": 1,
  "status": "active",
  "frequency": "monthly",
  "start_at": "2026-09-01",
  "end_at": null,
  "next_billing_at": "2026-09-01",
  "created_at": "2026-08-06T11:01:21.695Z",
  "billing_requests": []
}

From here, CoinGate takes over. A daily job checks every active schedule, and on each due date it generates a billing request and emails the customer a pay link. You do not cron anything.

Handling the callback

Set a callback_url and you will get a POST every time a billing request is created or changes status. That is how your app knows a subscriber actually paid this cycle.

The payload tells you what happened:

{
  "id": 3,
  "uuid": "a88d2edc-...",
  "status": "completed",
  "order_id": 27365192,
  "billing_contact_id": 1,
  "external_contact_id": "user_AA1234",
  "price_amount": "49.0",
  "price_currency": { "id": 11, "symbol": "EUR" },
  "pay_amount": "0.00022086",
  "pay_currency": { "id": 8, "symbol": "BTC" },
  "created_at": "2026-09-01T18:22:28.806Z"
}

The key field is status. A billing request moves through scheduled (created for a future date, and a state only recurring invoices have), pending (sent, awaiting payment), completed (paid), expired (the pay window lapsed) and canceled. Grant access on completed. And notice external_contact_id comes back in the payload, so you can flip the right user’s subscription to active without a lookup.

As with every CoinGate callback, verify it and make your handler idempotent, the same discipline covered in the developer integration guide.

Stopping a subscription

When a customer cancels, you discontinue the schedule with a PATCH:

curl --request PATCH "https://api.coingate.com/v2/billing/recurrings/1/discontinue" 
  -H "Authorization: Token YOUR_API_TOKEN"

This stops future billings and cancels any scheduled-but-not-yet-sent invoices. Invoices already issued are left alone, which is the behaviour you want, and chasing them stays your job. The schedule’s status becomes canceled.

What you settle in is a separate decision

Worth separating two things that get muddled. currency_id is the price the customer sees. receive_currency_id is what lands on your side. A subscription priced in euro can settle in euro, in USDC, or in whatever the customer paid with, and none of that changes the invoice.

If you would rather keep the original coins and sweep them later on your own schedule, that is a job for the Convert API rather than the billing setup.

Where this fits

Be honest with yourself about what you have built. This is recurring invoicing, not a silent card-on-file charge. The customer still pays each cycle. What you have removed is the manual work of creating and chasing those invoices, and the card-specific failure modes that quietly kill subscriptions: expired cards, issuer declines, cross-border rejections. For the wider picture of using crypto to recover that kind of churn, see our writeup on recurring crypto payments. The full endpoint reference lives in the Billing API docs.

Frequently asked questions

Can I charge a crypto subscription automatically like a card?

Not silently. Blockchain payments are push-based, so you cannot pull funds from a wallet on your own. The Billing API automates generating and sending an invoice each cycle, and the customer pays it. That is what recurring crypto billing means in practice.

What is the difference between a billing contact, product and recurring billing?

A contact is the customer, a product is an optional reusable priced line item, and a recurring billing is the schedule that automatically generates and sends invoices to a contact at a set frequency.

What billing frequencies are supported?

Weekly and monthly. The start_at date sets both the first invoice date and the anchor day for every invoice after it, resolving to the last valid day in months where that day does not exist.

How do I know when a subscriber has paid?

Set a callback_url on the recurring billing. CoinGate posts to it whenever a billing request changes status. Grant access when the status is completed, and use the returned external_contact_id to identify the user.

How do I cancel a subscription?

Send a PATCH to /v2/billing/recurrings/{id}/discontinue. It stops future and scheduled invoices while leaving already-issued ones untouched, and sets the schedule status to canceled.

Wrapping up

Recurring crypto billing is less magic and more automation. Create a contact, optionally a product, then a schedule with a frequency and a start date. CoinGate generates and sends each invoice, your callback tells you who paid, and a single PATCH ends it when someone churns. It will not pull from a wallet the way a card lets you, but it takes the invoicing grind off your plate and sidesteps the card failures that lose subscribers for no good reason.

Building subscriptions that accept crypto? Start with us.

VB
Vilius Barbaravičius Posted: August 6, 2026
Share article

Accept crypto with CoinGate

Accept crypto with confidence using everything you need in one platform.