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.
Every API call in this guide targets https://sandbox.ottu.net. Swap in your own merchant domain when you integrate.
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.
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
- Merchant creates a subscription — one Checkout API call with
payment_type: "auto_pay", anagreementobject, and anautopayblock describing the schedule. - 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. - Subscription created — the 201 response carries
extra.autopay.subscription_id, your handle on the subscription going forward. - 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. - 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.
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_debit — agreement.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).
- cURL
- Python
- Node.js
- PHP
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"
}
}'
import requests
response = requests.post(
"https://sandbox.ottu.net/b/checkout/v1/pymt-txn/",
headers={
"Authorization": "Api-Key <YOUR_API_KEY>",
"Content-Type": "application/json",
},
json={
"type": "e_commerce",
"amount": "9.900",
"payment_type": "auto_pay",
"currency_code": "KWD",
"pg_codes": ["credit-card"],
"customer_id": "cust_123",
"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",
},
},
)
session = response.json()
const response = await fetch("https://sandbox.ottu.net/b/checkout/v1/pymt-txn/", {
method: "POST",
headers: {
Authorization: "Api-Key <YOUR_API_KEY>",
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "e_commerce",
amount: "9.900",
payment_type: "auto_pay",
currency_code: "KWD",
pg_codes: ["credit-card"],
customer_id: "cust_123",
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",
},
}),
});
const session = await response.json();
$response = file_get_contents(
'https://sandbox.ottu.net/b/checkout/v1/pymt-txn/',
false,
stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Authorization: Api-Key <YOUR_API_KEY>\r\nContent-Type: application/json\r\n",
'content' => json_encode([
'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',
],
]),
],
])
);
$session = json_decode($response, true);
{
"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:
| Field | Type | Required | Notes |
|---|---|---|---|
frequency | string | Yes | monthly or yearly only. |
recurring_amount | decimal | Yes | Max 14 digits, 3 decimal places, min 0. |
start_date | date | Yes | YYYY-MM-DD. |
end_date | date | No | Nullable; must be strictly after start_date. |
description | string | No | Max 255 characters. |
retry_count | integer | No | 1–10. Overrides the default of 3 retries (4 total attempts) — see Retries & Dunning. |
notification_preferences | object | No | Per-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.
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:
{
"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.
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
curl --location 'https://sandbox.ottu.net/b/pbl/v2/subscriptions/sub_abc123/' \
--header 'Authorization: Api-Key <YOUR_API_KEY>'
{
"id": "sub_abc123",
"customer_id": "cust_12345",
"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.
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.
The customer self-service page (below) can only schedule a cancellation for the end of the period, never immediately — see Customer Self-Service Page.
5. Manage the customer self-service link
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:
curl --location --request POST 'https://sandbox.ottu.net/b/pbl/v2/subscriptions/sub_abc123/regenerate-page-token/' \
--header 'Authorization: Api-Key <YOUR_API_KEY>'
{
"page_token": "new_signed_token_here",
"page_url": "https://merchant.example.com/en/subscription/new_signed_token_here"
}
curl --location 'https://sandbox.ottu.net/b/pbl/v2/subscriptions/sub_abc123/page-link/' \
--header 'Authorization: Api-Key <YOUR_API_KEY>'
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
curl --location 'https://sandbox.ottu.net/b/pbl/v2/subscriptions/sub_abc123/cycles/?limit=10&offset=0' \
--header 'Authorization: Api-Key <YOUR_API_KEY>'
{
"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:
| State | Meaning |
|---|---|
pending_setup | Created by the checkout call; waiting on the customer to complete the CIT. |
trialing | The CIT settled with a 0.000 first charge — no money moved yet, but the schedule is live. |
active | Billing normally. MIT charges run on schedule. |
past_due | A billing cycle exhausted every retry. See Retries & Dunning. |
canceled | Terminal. No more charges, ever. |
expired | Terminal. The subscription's end_date was reached naturally. |
setup_failed | The 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.
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:
| Key | Controls |
|---|---|
pre_charge_enabled | The upcoming-charge reminder. |
failure_enabled | The payment-failed email sent while retries remain. |
final_failure_enabled | The 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_dueback toactive). - Cancel, or undo a pending cancellation.
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
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_urlyourself, 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_dueas 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_preferenceskeys 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_timedefaults 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_idinto a query string yourself.
FAQ
What's Next?
- Recurring Payments & Auto-Debit — the alternative when you want to own the billing schedule yourself.
- Checkout API — the full field reference for the call that creates a subscription.
- Webhooks — Payment Events — notifications for the underlying payment sessions AutoPay creates.
- AutoPay for merchants — the business-side overview of the same feature.