Skip to content

Duesly (payments)

Put a customer on a recurring plan, read what they owe, and record a payment collected outside the gateway — from your own billing system.

Open Duesly
POST/api/v1/apps/duesly/subscribers

Duesly collects recurring payments over WhatsApp. These endpoints are for when the customer list already lives somewhere else, such as your billing system, a spreadsheet pipeline or a POS, and you want Duesly to do the chasing without anyone retyping it.

Method Path Scope Purpose
POST /api/v1/apps/duesly/subscribers campaigns:write Put a customer on a recurring plan.
GET /api/v1/apps/duesly/subscribers campaigns:read List them.
GET /api/v1/apps/duesly/dues campaigns:read What is owed, and what has been paid.
POST /api/v1/apps/duesly/dues/{due_id}/paid campaigns:write Record a payment collected elsewhere.

Duesly is part of Campaigns, so it uses the campaign scopes rather than its own.

The app has to be installed #

Every endpoint here answers 412 when the workspace hasn't installed Duesly:

412 Precondition Failed
{
"error": {
"code": "failed_precondition",
"message": "The Duesly app is not installed in this workspace.",
"details": { "reason": "not_found", "docs": "https://docs.amomic.in/apps/overview" },
"request_id": "req_01JRBC2D5F8H1K4M7P0R3T6V9X",
"correlation_id": "cor_01JRBC2D5F8H1K4M7P0R3T6V9X"
}
}

That is deliberately not a 404: a 404 on a workspace that hasn't installed the app would look the same as a typo in the URL. Install it from Campaigns → Apps; see Apps.

Reminders, follow-ups, templates and how you get paid are set in the console, not here. The API adds and reads the data; the app owns the behaviour.

Adding a subscriber #

namestringrequired

1–140 characters. Appears in the reminder.

phonestringrequired

6–24 characters. With a country code, or a local number in your workspace's country. Refused rather than guessed if it's neither.

emailstringoptionaldefault null

Up to 200 characters. Stored on the subscriber and their contact.

plan_namestringoptionaldefault Monthly plan

Up to 120 characters. What they are paying for. Appears in the reminder.

amountnumberrequired

In rupees. More than 0, up to 1,00,00,000.

cycle_unitstringoptionaldefault monthly

How often they pay.

weeklymonthlyquarterlyhalf_yearlyyearlycustom_days
cycle_daysintegeroptionaldefault null

1–730. Required for custom_days.

due_dayintegeroptionaldefault null

1–31. Day of the month for calendar cycles. A day the month doesn't have becomes its last day.

start_datestringoptionaldefault today

YYYY-MM-DD. When the first payment is due. Omitted, today in your workspace's time zone.

Shell
curl -X POST https://services.amomic.in/api/v1/apps/duesly/subscribers \
-H "Authorization: Bearer $AMOMIC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sub-VH-CUST-40718" \
-d '{
"name": "Rohit Deshpande",
"phone": "+919823016745",
"email": "rohit.deshpande@example.in",
"plan_name": "AC annual maintenance (2 units)",
"amount": 749.5,
"cycle_unit": "monthly",
"due_day": 5,
"start_date": "2026-11-05"
}'

201 Created, with the subscriber:

201 Created
{
"subscriber_id": "sub_01JRBC6G9J2L5N8Q1S4U7W0Y3A",
"tenant_id": "org_veloria_home",
"contact_id": "cnt_01JRBC6H0K3M6P9R2T5V8X1Z4B",
"contact_pending": false,
"name": "Rohit Deshpande",
"phone": "+919823016745",
"email": "rohit.deshpande@example.in",
"plan_name": "AC annual maintenance (2 units)",
"amount": 749.5,
"currency": "INR",
"cycle_unit": "monthly",
"cycle_days": null,
"due_day": 5,
"start_date": "2026-11-05",
"status": "active",
"notes": null,
"tags": [],
"next_due_date": "2026-11-05",
"last_paid_at": null,
"collected_total": 0,
"outstanding_total": 1499.0,
"dues_paid": 0,
"dues_declined": 0,
"version": 2,
"created_at": "2026-10-14T06:21:48.093517+00:00",
"updated_at": "2026-10-14T06:21:48.611204+00:00"
}

Two things happen beyond the write:

  • They become a contact, so they appear in Contacts and every conversation with them sits in one place.
  • Their next two payments are raised immediately, so next_due_date and outstanding_total are already filled in and a GET /dues right after this returns them, rather than waiting for the next scheduled run. payment.due_created fires for each.

The phone number is stored in E.164. status is active, paused or cancelled; pausing and cancelling a subscriber happen in the console.

One subscriber per phone number

A second subscriber on the same number is refused with 409 and duplicate_recipient, and details.subscriber_id names the existing one. That stops two subscriptions sending two reminders for one payment.

If the contact can't be linked, because the directory couldn't be reached or the details belong to two different existing contacts, the subscriber is still created, with contact_id: null and contact_pending: true. Reminders still go out, and the console offers a retry. A directory blip must not stop a business taking payments.

POST /subscribers honours Idempotency-Key. Set it to your own customer id so a re-run of your sync is safe. See Idempotency.

Listing subscribers #

GET/api/v1/apps/duesly/subscribers
Query Default Notes
limit 25 1–200.
cursor none The next_cursor from the previous page.

Newest first. Returns {"items": [subscriber, …], "next_cursor": "…"}, each item shaped like the create response. There are no filters. See Pagination.

Reading what is owed #

GET/api/v1/apps/duesly/dues
Query Default Notes
scope none open, overdue, upcoming or paid. Anything else is a 400.
status none One status from the table below. When you send both, status wins and scope is ignored.
limit 25 1–200.
cursor none The next_cursor from the previous page.
scope Returns Order
none Every due Latest due date first
open Not yet settled: scheduled, reminded, followup_1, followup_2 Latest due date first
overdue Open, with a due date before today Oldest first
upcoming scheduled, due today or later Soonest first
paid paid Latest due date first

"Today" is today in your workspace's time zone.

Shell
curl -G https://services.amomic.in/api/v1/apps/duesly/dues \
-H "Authorization: Bearer $AMOMIC_API_KEY" \
--data-urlencode "scope=overdue" \
--data-urlencode "limit=100"
200 OK
{
"items": [
{
"due_id": "due_4c81e07b2f9a63d5c18e47b20a",
"tenant_id": "org_veloria_home",
"subscriber_id": "sub_01JQ7T2V5X8A1C4E7G0J3L6N9Q",
"subscriber_name": "Sneha Pillai",
"phone": "+919946102385",
"contact_id": "cnt_01JQ7T2W6Y9B2D5F8H1K4M7P0R",
"period_label": "October 2026",
"amount": 1249.0,
"currency": "INR",
"due_date": "2026-10-05",
"status": "followup_1",
"collection_mode": "manual_upi",
"payment_link": null,
"payment_link_id": null,
"payment_reference": null,
"paid_at": null,
"paid_amount": null,
"marked_by": null,
"marked_by_name": null,
"reminders_sent": 2,
"last_reminder_at": "2026-10-06T04:30:12.507311+00:00",
"next_action_at": "2026-10-07T04:30:00+00:00",
"campaign_id": "cmp_01JRAZ1B4D7F0H3K6M9P2R5T8V",
"failure_reason": null,
"version": 3,
"created_at": "2026-10-01T00:30:04.118205+00:00",
"updated_at": "2026-10-06T04:30:12.507311+00:00"
}
],
"next_cursor": null
}
Field Notes
due_id due_ followed by 26 hex characters. The id for marking it paid.
period_label October 2026 for monthly, quarterly, half-yearly and yearly plans; a date such as 12 Oct 2026 for weekly and custom cycles.
amount, currency What is owed. Always INR.
status See the ladder below.
collection_mode manual_upi (your own UPI or QR) or razorpay (a payment link). See How you get paid.
payment_link The link in the reminder, in razorpay mode.
paid_at, paid_amount, payment_reference Set once it's paid.
marked_by, marked_by_name Who recorded a manual payment.
reminders_sent, last_reminder_at, next_action_at Where the chase is.
campaign_id The reminder campaign that last messaged them.
version Needed to mark it paid.

The statuses are a ladder, and it is finite:

status Meaning
scheduled Raised, not yet due.
reminded First reminder sent.
followup_1 / followup_2 One or two follow-ups sent.
paid Settled.
declined We have stopped asking. Not "the customer refused".
waived The business decided not to collect it.
cancelled The subscription ended before this payment came around.

A payment gets one reminder and at most two follow-ups. See the reminder ladder: the limit is fixed and no API call raises it.

Recording a payment #

POST/api/v1/apps/duesly/dues/{due_id}/paid

For money collected outside the payment link: cash, a bank transfer, UPI to your own account, a cheque. With a payment gateway connected, link payments settle themselves and you don't need this.

expected_versionintegerrequired

The due's version from your last read. 1 or more.

amountnumberoptionaldefault the amount owed

What actually arrived, more than 0. Stored as paid_amount. The due is paid either way.

referencestringoptionaldefault null

Up to 140 characters. A UPI transaction id, a cheque number: whatever you would look up later.

paid_atstringoptionaldefault now

ISO 8601.

Shell
curl -X POST https://services.amomic.in/api/v1/apps/duesly/dues/due_4c81e07b2f9a63d5c18e47b20a/paid \
-H "Authorization: Bearer $AMOMIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expected_version": 3,
"amount": 1249,
"reference": "UPI 428617093512",
"paid_at": "2026-10-14T10:42:00+05:30"
}'

200 OK, with the due as it now stands: status paid, paid_amount, paid_at, payment_reference, and version one higher. The subscriber's totals are updated, the chase stops, and payment.paid fires. If you've set up a receipt in Duesly, the customer gets it.

Recording a payment also tries to cancel the due's live payment link, so nobody pays twice. That cancel is best-effort: if it fails, the payment is still recorded.

Idempotent by state, not by key #

This endpoint ignores Idempotency-Key. A due can only be marked paid while it's open, so a retried call can't rewrite a settled payment:

Situation Response
The due is open and expected_version matches 200, the due is now paid.
expected_version is stale (someone or something changed the due) 409 conflict, reason: "version_conflict", with details.current_version.
The due is already paid, declined, waived or cancelled 412 failed_precondition, reason: "conflict", with details.current_status. Message: This payment is already paid.
No such due in this workspace 404 not_found, Payment not found.

The version is checked first. So a retry of a call that already worked usually gets the 409, because marking it paid moved the version on. In both cases, read the due again: if it's paid, your payment, or someone else's, is already recorded.

Failures #

400invalid_argumentinvalid_destinationCreate: the phone number isn't usable. Include the country code, or write it as a local number in your workspace's country.
400invalid_argument`Request validation failed.` A field outside its limits, `custom_days` without `cycle_days`, an unknown `scope`, or a `limit` outside 1–200. `details.errors` names it.
401unauthenticatedThe key is missing, malformed, unknown, revoked, past its roll grace period, or for the wrong environment. `details.reason` says which.
403permission_deniedmissing_scopeReads need `campaigns:read`; creating a subscriber and marking paid need `campaigns:write`.
403permission_deniedworkspace_suspendedThe workspace is suspended. Restore billing to use its keys.
404not_found`Payment not found.` Mark paid on an unknown due.
409conflictduplicate_recipientCreate: a subscriber already uses this phone number. `details.subscriber_id` names them.
409conflictversion_conflictMark paid with a stale `expected_version`.
409conflictidempotency_key_reuseCreate: the same `Idempotency-Key` with a different body.
412failed_preconditionnot_foundDuesly isn't installed in this workspace. `details.docs` links the Apps page.
412failed_preconditionconflictMark paid on a due that is no longer open. `details.current_status` says what it is.
429rate_limitedrate_limited120 requests per minute per key. See [Rate limits](/api/rate-limits).

Events #

The reason to integrate here rather than poll:

Event When Payload
payment.due_created A payment is raised by the cycle due_id, subscriber_id, amount, currency, period, due_date
payment.paid Settled due_id, subscriber_id, amount, period, reference, source
payment.declined The chase ended unpaid due_id, subscriber_id, amount, period

payment.due_created fires the moment a payment is raised, so your own ledger can mirror the invoice without polling for it.

Don't rely on `source` for payments you record

payment.paid carries source, gateway or manual. A payment marked in the console reports manual and a payment link settlement reports gateway, but a payment you record with this API currently also reports gateway. To tell your own settlements apart, match on due_id or on the reference you sent.

What the payload doesn't carry

No payment link, no gateway credentials, and nothing about the card. The projection names the fields it forwards rather than forwarding whatever the internal event holds.

See Webhooks and Verifying signatures.

A worked integration #

The shape that works for most billing systems:

  1. 1

    Mirror your customers once

    POST /subscribers for each active plan, with Idempotency-Key set to your own customer id so a re-run is safe. Treat 409 duplicate_recipient as "already there" and store the subscriber_id it returns.

  2. 2

    Subscribe to the three payment events

    Your ledger stays in step without polling.

  3. 3

    Let Duesly do the chasing

    Reminders, follow-ups and the payment link are the app's job. You don't need to call anything on a schedule.

  4. 4

    Push settlements you collect elsewhere

    When money arrives in your own system, read the due for its version, then POST /dues/{due_id}/paid so the chasing stops.