Skip to main content

AutoPay

AutoPay is Ottu's fully managed subscription engine. With Recurring Payments & Auto-Debit, you own the billing schedule — your system decides when to charge the saved card, retries failures, and emails the customer. With AutoPay (payment_type: "auto_pay"), Ottu owns all of that instead: you make one Checkout API call to set up the subscription, and from there AutoPay generates each billing cycle, retries failed charges, notifies the customer, and gives them a self-service page — no scheduler, retry loop, or dunning emails to build yourself.

Merchant-facing management (list, retrieve, cancel, and the rest) is REST-API-only in this phase — there is no AutoPay dashboard screen yet. Every call on this page, including the six subscription-management endpoints, uses the standard Ottu API key flow — there is no separate credential to obtain.

Base URL

Every API call in this guide targets https://sandbox.ottu.net. Swap in your own merchant domain when you integrate.

Boost Your Integration

Ottu offers SDKs and tools to speed up your integration. See Getting Started for all available options.

When to Use

  • Subscriptions and SaaS billing — monthly or yearly plans where you don't want to build and operate a billing scheduler.
  • You want Ottu to handle dunning — failed-charge retries, the past-due state, and recovery emails happen automatically.
  • You want a customer self-service surface without building one — a hosted page for viewing status, paying an outstanding balance, updating the card, or canceling.
  • You have no plan catalog to manage — there's no "create a plan" step; pricing and scheduling are just fields on your checkout call.
Choosing between AutoPay and Auto-Debit

Use AutoPay when you want Ottu to own the whole subscription lifecycle after one checkout call. If you'd rather run the billing schedule and retry logic yourself and just charge a saved card on demand, use Recurring Payments & Auto-Debit instead — it's the closer fit for installments, on-demand billing, and cases where your own system needs to decide exactly when a charge happens.

Guide

Workflow

AutoPay subscription flow The merchant makes one Checkout API call with payment_type auto_pay, and AutoPay creates the subscription synchronously. The merchant redirects the customer to the checkout page, where the customer enters their card and pays the first charge, the CIT. From then on the billing cycles, retries, dunning, and the customer self-service page are all handled for the merchant, who makes no further calls to keep the subscription running. OTTU PLATFORM Customer Merchant Backend Your server Checkout Page Ottu-hosted or SDK embed AutoPay Creates the subscription Handled for you Billing cycles · retries Dunning · notifications Customer self-service page checkout call · auto_pay redirect to checkout enters card · pays (CIT) submits payment ongoing
  1. Merchant creates a subscription — one Checkout API call with payment_type: "auto_pay", an agreement object, and an autopay block describing the schedule.
  2. The subscription is created synchronously — you get the result in the same response, not later by webhook. If the subscription can't be created, the transaction is marked failed and you get a 4xx with no usable session_id.
  3. Subscription created — the 201 response carries extra.autopay.subscription_id, your handle on the subscription going forward.
  4. Customer pays the CIT — you redirect the customer to the checkout page (Checkout SDK or hosted page) from the session_id, same as any other Checkout API session. They enter their card and complete the first charge.
  5. AutoPay owns everything after — billing cycles, retries, dunning emails, and the customer self-service page. You don't call anything to keep the subscription running.

Step-by-Step

One credential covers everything here. Step 1 (creating a subscription) is a normal Checkout API call. Steps 2–6 (managing subscriptions you've already created) use /b/pbl/v2/subscriptions/ on the same host. Both authenticate the same way, with the API key you already use:

Authorization: Api-Key <YOUR_API_KEY>

There is no second credential to obtain and no separate endpoint to configure.

Two different surfaces, one host

Both the merchant API and the customer self-service page live on your Connect domain, at different paths. The merchant endpoints below sit under /b/pbl/v2/subscriptions/. The customer page is served at /<lang>/subscription/<page_token> — see Customer Self-Service Page. Your integration only ever calls the first; the second is what you hand to the customer.

1. Create a subscription

Call the Checkout API with payment_type: "auto_pay". Three things are easy to miss: agreement is required and — unlike auto_debitagreement.id is required inside it too; customer_id is required; and customer_email is required (see Error Handling below for what happens if you skip it).

Create an AutoPay subscription
curl --location 'https://sandbox.ottu.net/b/checkout/v1/pymt-txn/' \
--header 'Authorization: Api-Key <YOUR_API_KEY>' \
--header 'Content-Type: application/json' \
--data '{
"type": "e_commerce",
"amount": "9.900",
"payment_type": "auto_pay",
"currency_code": "KWD",
"pg_codes": ["credit-card"],
"customer_id": "cust_123",
"customer_email": "[email protected]",
"webhook_url": "https://yourwebsite.com/webhook",
"agreement": {
"id": "AGR-sub-001"
},
"autopay": {
"frequency": "monthly",
"recurring_amount": "9.900",
"start_date": "2026-09-01",
"description": "Pro Plan — Monthly"
}
}'
Example response — 201 Created
{
"session_id": "sess_9f8e7d6c5b4a",
"checkout_url": "https://sandbox.ottu.net/checkout/sess_9f8e7d6c5b4a",
"checkout_short_url": "https://sandbox.ottu.net/c/sess_9f8e7d6c5b4a",
"expiration_time": "00:30:00",
"extra": {
"autopay": {
"subscription_id": "sub_hA1YYmZz1xk8speCiPbOquIP"
}
}
}

extra.autopay.subscription_id is your handle on the subscription — save it. The autopay block you sent is write-only; it is not echoed back anywhere in the response, so don't build logic that expects to read frequency or recurring_amount off the checkout response. Use Retrieve a subscription for that.

The autopay request block:

FieldTypeRequiredNotes
frequencystringYesmonthly or yearly only.
recurring_amountdecimalYesMax 14 digits, 3 decimal places, min 0.
start_datedateYesYYYY-MM-DD.
end_datedateNoNullable; must be strictly after start_date.
descriptionstringNoMax 255 characters.
retry_countintegerNo1–10. Overrides the default of 3 retries (4 total attempts) — see Retries & Dunning.
notification_preferencesobjectNoPer-subscription email toggles — pre_charge_enabled, failure_enabled, final_failure_enabled, booleans defaulting to true. Connect does not validate it. See Notifications.

Fields you don't send are dropped rather than sent as null, so AutoPay applies its own defaults instead of a default you didn't intend.

Zero-amount subscriptions start a trial

amount may be "0.000" on the checkout call as long as type isn't one_off — this creates the subscription with no first charge, and it starts in trialing instead of active. See Subscription States.

Error Handling: Missing customer_email

customer_email looks optional on a normal Checkout API call, but AutoPay requires it. Omit it and AutoPay's internal validation rejects the request at bind time; Connect surfaces that as:

Response — 400 Bad Request
{
"autopay": ["AutoPayClient rejected request (422)"]
}

This error reads like something is wrong with your agreement object — it isn't. Check customer_email first; it's the most common trigger for this exact message.

2. List subscriptions

POST /b/pbl/v2/subscriptions/ — yes, a POST that only reads. Filter parameters live in the request body on purpose, so customer_id never lands in server access logs.

List active subscriptions for a customer
curl --location 'https://sandbox.ottu.net/b/pbl/v2/subscriptions/' \
--header 'Authorization: Api-Key <YOUR_API_KEY>' \
--header 'Content-Type: application/json' \
--data '{
"customer_id": "cust_12345",
"status": "active",
"limit": 50
}'

All filter fields are optional: status, customer_id, created_after, created_before, limit (default 20, max 100), offset. The response is the standard paginated envelope — count, next, previous, results. Each item in results is a slim summary — id, customer_id, customer_email, frequency, recurring_amount, currency, status, next_billing_date, created_at — not the full subscription object. For the active card and latest-cycle detail, call Retrieve a subscription with the item's id.

3. Retrieve a subscription

Get subscription detail
curl --location 'https://sandbox.ottu.net/b/pbl/v2/subscriptions/sub_abc123/' \
--header 'Authorization: Api-Key <YOUR_API_KEY>'
Example response — 200 OK
{
"id": "sub_abc123",
"customer_id": "cust_12345",
"customer_email": "[email protected]",
"customer_name": "Jane Doe",
"customer_phone": "+96550000000",
"frequency": "monthly",
"currency": "KWD",
"recurring_amount": "49.990",
"first_payment_amount": "49.990",
"start_date": "2026-04-24",
"end_date": "2027-04-24",
"billing_anchor_day": 24,
"setup_session_id": "sess_9f8e7d6c5b4a",
"agreement_id": "AGR-abc123",
"amount_variability": "fixed",
"cycle_interval_days": 30,
"total_cycles": 12,
"notification_config": null,
"status": "active",
"cancel_at_period_end": false,
"canceled_at": null,
"cancellation_initiator": null,
"cancellation_reason": null,
"cancellation_note": null,
"next_billing_date": "2026-05-24",
"description": "Pro Plan — Monthly",
"merchant_reference": "ref-001",
"plan_name": "Pro Plan",
"language": "en",
"retry_count": 3,
"created_at": "2026-04-24T10:30:00Z",
"updated_at": "2026-04-24T10:30:00Z",
"card": {"masked_card": "**** 1111", "card_brand": "visa"},
"current_cycle": {
"cycle_number": 1,
"scheduled_date": "2026-04-24",
"amount_due": "49.990",
"status": "paid",
"paid_at": "2026-04-24T10:31:00Z",
"session_id": "sess_prev001",
"charge_type": "cit"
}
}

status, card, and current_cycle are the three fields worth polling — together they tell you whether the CIT completed, which card is active, and how the latest charge went.

4. Cancel a subscription

POST /b/pbl/v2/subscriptions/{subscription_id}/cancel/ requires an Idempotency-Key header (UUID v4) — this is a mutation, and a retried network call must not double-cancel.

Cancel immediately
curl --location 'https://sandbox.ottu.net/b/pbl/v2/subscriptions/sub_abc123/cancel/' \
--header 'Authorization: Api-Key <YOUR_API_KEY>' \
--header 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \
--header 'Content-Type: application/json' \
--data '{"mode": "immediate"}'

mode: "immediate" stops the subscription right away — status becomes canceled and no more cycles are scheduled. mode: "at_period_end" instead sets cancel_at_period_end: true and leaves status as-is; the subscription keeps billing through the current period and only flips to canceled when the next cycle would have been generated.

Only the merchant can cancel immediately

The customer self-service page (below) can only schedule a cancellation for the end of the period, never immediately — see Customer Self-Service Page.

The merchant delivers a per-subscription link to the customer; the signed token in the URL is the credential — see Customer Self-Service Page for what the customer can do with it. Two endpoints manage that link:

Regenerate the page token (revokes the old link instantly)
curl --location --request POST 'https://sandbox.ottu.net/b/pbl/v2/subscriptions/sub_abc123/regenerate-page-token/' \
--header 'Authorization: Api-Key <YOUR_API_KEY>'
Example response — 200 OK
{
"page_token": "new_signed_token_here",
"page_url": "https://merchant.example.com/en/subscription/new_signed_token_here"
}
Read the current link without rotating it
curl --location 'https://sandbox.ottu.net/b/pbl/v2/subscriptions/sub_abc123/page-link/' \
--header 'Authorization: Api-Key <YOUR_API_KEY>'
Regenerating revokes the old link instantly — and silently

The old page_token stops working the moment you call regenerate — before you've delivered the new one. AutoPay does not notify the customer. You must send them the new page_url yourself, or they'll hit a dead link. Use page-link (not regenerate-page-token) when you just need to re-send a link that should keep working — regenerating is only for revoking a compromised or lost one.

Note also that regenerate-page-token is deliberately not idempotent — each call mints a new token and kills the old one, so it does not take (or need) an Idempotency-Key header. Retrying it on a timeout will revoke a link a second time.

6. Billing cycle history

List billing cycles
curl --location 'https://sandbox.ottu.net/b/pbl/v2/subscriptions/sub_abc123/cycles/?limit=10&offset=0' \
--header 'Authorization: Api-Key <YOUR_API_KEY>'
Example response — 200 OK
{
"count": 5,
"next": null,
"previous": null,
"results": [
{
"cycle_number": 1,
"scheduled_date": "2026-05-24",
"amount_due": "49.990",
"status": "paid",
"paid_at": "2026-05-24T15:30:00Z",
"failure_reason": null,
"session_id": "sess_abc123",
"charge_type": "mit",
"payment_attempts": []
}
]
}

Cycle status values: scheduled (charge pending), processing (charge in flight), retry_scheduled (an attempt failed and the next automatic retry is queued), paid, failed (all retries exhausted — the subscription is now past_due), and canceled (the cycle was canceled along with the subscription). See Billing Cycles for how cycles are generated and Retries & Dunning for the retry schedule that gets a cycle from scheduled to failed.

Use Cases

Subscription States

A subscription is always in exactly one of seven states:

StateMeaning
pending_setupCreated by the checkout call; waiting on the customer to complete the CIT.
trialingThe CIT settled with a 0.000 first charge — no money moved yet, but the schedule is live.
activeBilling normally. MIT charges run on schedule.
past_dueA billing cycle exhausted every retry. See Retries & Dunning.
canceledTerminal. No more charges, ever.
expiredTerminal. The subscription's end_date was reached naturally.
setup_failedThe CIT never completed. Not terminal — retrying setup moves it back to pending_setup.

Two behaviors worth calling out explicitly: a subscription only reaches active via a nonzero first charge — a 0.000 first charge starts it in trialing instead. And a subscription only becomes past_due after every retry on the current cycle has been exhausted, never on the first failure. Once past_due, the customer paying the outstanding balance (via the self-service page) moves it straight back to active.

Billing Cycles

Cycles are generated one at a time — the next cycle isn't created until the previous one is paid. Each cycle snapshots its own amount_due when it's created, so a cycle always charges the amount it was scheduled for, not whatever the subscription's current recurring_amount happens to be by the time it runs.

The billing date is anchor-day clamped: a subscription anchored on the 31st that bills into February charges on the 28th (or 29th in a leap year) — then returns to the 31st the next time a 31-day month comes around. The anchor day itself never moves; only the day-of-month is clamped when the month is shorter.

Retries & Dunning

If a charge fails, AutoPay retries. The default is 3 retries — 4 total attempts (1 initial + 3 retries) — configurable per subscription via retry_count (1–10) on the autopay block. Retries are spaced one hour apart, flat — there's no exponential backoff.

When every attempt on a cycle is exhausted: the cycle moves to failed, the subscription moves to past_due, and the customer gets a final-failure email with a recovery link.

AutoPay never auto-cancels a past-due subscription

This is the most surprising behavior in the product, so it's worth stating plainly: a past_due subscription stays past_due indefinitely. AutoPay does not cancel it for you. It only leaves that state when the customer pays the outstanding balance, or you cancel it yourself via Cancel a subscription. If your business logic assumes past-due subscriptions eventually self-clean, build that check yourself.

Notifications

AutoPay sends three email types, each available in English and Arabic:

  • Upcoming-charge reminder — sent before a scheduled charge.
  • Payment failed — sent after a retry attempt fails (but before all retries are exhausted).
  • Final failure — sent when a cycle's retries are exhausted, with a recovery link to the self-service page.

Reminder timing depends on frequency: monthly subscriptions get one reminder, 2 days before the charge; yearly subscriptions get three, at 30, 10, and 3 days before. Each of the three email types can be turned off independently per subscription via notification_preferences on the autopay block. The object takes three boolean keys, each defaulting to true:

KeyControls
pre_charge_enabledThe upcoming-charge reminder.
failure_enabledThe payment-failed email sent while retries remain.
final_failure_enabledThe final-failure email sent when retries are exhausted.

Because Connect doesn't validate the object, a typo'd key fails silently rather than erroring — an unrecognized key is simply ignored and the default stays on.

Customer Self-Service Page

The page_url from step 5 is a credential — anyone holding it can act on the subscription. From it, a customer can:

  • View status, plan, and next charge date.
  • View billing history.
  • Add a card or switch the active card.
  • Pay an outstanding balance (moves past_due back to active).
  • Cancel, or undo a pending cancellation.
Customer cancellation is always end-of-period

Unlike the merchant Cancel endpoint, a cancellation initiated from the self-service page always takes effect at the end of the current billing period — never immediately. Only the merchant-side mode: "immediate" call stops a subscription right away.

API Reference

Interactive reference coming soon

AutoPay's endpoints aren't in the public OpenAPI schema yet, so there's no interactive explorer here for the moment. See Step-by-Step for the full set of six endpoints with cURL examples. The request bodies, responses, and behaviour documented there are accurate; the path the six management endpoints sit behind is not yet final, and is flagged in that section.

Best Practices

  • Send Idempotency-Key (UUID v4) on Cancel — but not on Regenerate Page Token, which is deliberately non-idempotent. Sending one there does nothing; retrying the call on a timeout revokes a link a second time.
  • Deliver page_url yourself, every time. AutoPay never notifies the customer when you regenerate — a stale link left undelivered is a customer who can't self-serve.
  • Treat past_due as sticky. AutoPay never auto-cancels it — build your own monitoring or reminder cadence around subscriptions stuck in that state if your business needs one.
  • Match notification_preferences keys exactly. Connect passes the object through without validating it, so a misspelled key silently does nothing instead of erroring. The three valid keys are listed under Notifications.
  • Don't assume card_acceptance_criteria.min_expiry_time defaults for you. Auto-debit defaults it to 30 days; AutoPay does not apply that default — set it explicitly if your integration relies on it.
  • Keep filters in the body on List. It's a POST for a reason — never move customer_id into a query string yourself.

FAQ

What's Next?