diff --git a/.stats.yml b/.stats.yml index 1fd2991..c7c071b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1 +1 @@ -configured_endpoints: 41 +configured_endpoints: 45 diff --git a/api.md b/api.md index 31bb7ea..d408acf 100644 --- a/api.md +++ b/api.md @@ -164,6 +164,21 @@ Methods: - client.available_number.retrieve(\*\*params) -> AvailableNumberRetrieveResponse +# PaymentRequests + +Types: + +```python +from linq.types import PaymentRequest, PaymentRequestListResponse +``` + +Methods: + +- client.payment_requests.create(\*\*params) -> PaymentRequest +- client.payment_requests.retrieve(payment_request_id) -> PaymentRequest +- client.payment_requests.list(\*\*params) -> PaymentRequestListResponse +- client.payment_requests.cancel(payment_request_id) -> PaymentRequest + # WebhookEvents Types: diff --git a/scripts/lint b/scripts/lint index 3232aa9..b0df0fd 100755 --- a/scripts/lint +++ b/scripts/lint @@ -13,7 +13,7 @@ else fi echo "==> Running pyright" -uv run pyright +uv run pyright -p . echo "==> Running mypy" uv run mypy . diff --git a/src/linq/_client.py b/src/linq/_client.py index 45286ad..05ed097 100644 --- a/src/linq/_client.py +++ b/src/linq/_client.py @@ -46,6 +46,7 @@ phone_numbers, webhook_events, available_number, + payment_requests, webhook_subscriptions, ) from .resources.messages import MessagesResource, AsyncMessagesResource @@ -58,6 +59,7 @@ from .resources.phone_numbers import PhoneNumbersResource, AsyncPhoneNumbersResource from .resources.webhook_events import WebhookEventsResource, AsyncWebhookEventsResource from .resources.available_number import AvailableNumberResource, AsyncAvailableNumberResource + from .resources.payment_requests import PaymentRequestsResource, AsyncPaymentRequestsResource from .resources.webhook_subscriptions import WebhookSubscriptionsResource, AsyncWebhookSubscriptionsResource __all__ = [ @@ -443,6 +445,126 @@ def available_number(self) -> AvailableNumberResource: return AvailableNumberResource(self) + @cached_property + def payment_requests(self) -> PaymentRequestsResource: + """Request a payment from a recipient over iMessage. + + You create a payment + request, send its `checkout_url` to the recipient, and they pay with Apple + Pay or card. Funds settle **directly to your own Stripe account** — Linq + never holds the money. + + ## How it works + + 1. **Create** a payment request with an amount and currency. You get back a + `checkout_url` and a `status` of `requested`. + 2. **Send** the `checkout_url` to the recipient as a `link` message part so + it arrives as a tappable card (see *Sending the link* below). + 3. The recipient **pays** on the hosted checkout (Apple Pay App Clip on a + supported iPhone, web checkout everywhere else). + 4. You receive a **`payment.succeeded`** webhook and the request's `status` + becomes `succeeded`. Requests you don't collect eventually `expire`. + + ## Connected accounts (Stripe Standard, direct charges) + + Agent Pay runs on **Stripe Connect Standard accounts** using **direct + charges**: the charge is created on *your* connected account and **you are + the merchant of record**. That means the money, the payout schedule, the + customer relationship, and the compliance surface are all yours — Linq + orchestrates the request and the checkout but is never in the funds flow. + + **Refunds, disputes, and chargebacks are handled by you, in your own Stripe + Dashboard.** Because charges settle directly to your account, Linq has no + custody of the funds and cannot issue refunds or contest disputes on your + behalf — and there is no refund/dispute endpoint in this API by design. Use + the Stripe Dashboard (or the Stripe API on your own account) for the money + lifecycle after a payment succeeds. + + ## Getting set up + + Open **Agent Pay** in your Linq dashboard + (`https://zero.linqapp.com/organization/payments`), click **Connect Stripe**, + and complete Stripe's onboarding (business details + a bank account). When + your account reaches `charges_enabled`, request creation unlocks; until you + connect Stripe, `POST /v3/payment_requests` returns `403`. You can keep + collecting even while Stripe finishes background verification. + + ## Subscriptions + + Set `mode: subscription` on `POST /v3/payment_requests` to start an + **auto-renewing subscription** instead of a one-time charge. Instead of an + amount, you pass a `price_id` — an active **recurring Price** on your + connected Stripe account (create one in your Stripe Dashboard under + Product catalog; if you sell through Stripe Payment Links today, reuse the + price your link is built from). The recipient pays the first invoice at + the same checkout, and their payment method is saved to the subscription + for automatic renewals. + + The division of labor is deliberate: **Linq handles the first payment, + your Stripe account handles the rest.** The request reaches `succeeded` + when the first invoice is paid; from then on the subscription lives + entirely on your connected account. The response's `stripe` object gives + you the join keys — `customer_id` and `subscription_id` — so renewals, + plan changes, dunning, and cancellation are managed with your own Stripe + Dashboard/API and your own Stripe webhooks. Your `metadata` is stamped on + the Customer and Subscription, so correlating in either direction is + trivial. There are no renewal webhooks from Linq by design. + + ### Free trials + + Add `trial_period_days` (or a fixed `trial_end` timestamp) to start the + subscription with a free trial. The checkout still collects the + recipient's payment method — the pay sheet shows "$0 due today" with the + first charge date — and saves it to the subscription; Stripe bills it + automatically when the trial ends. The request reaches `succeeded` when + the card is collected, and the response carries `trial_end`. If the trial + would end without a payment method on file, the subscription cancels + rather than generating unpayable invoices. Trial lifecycle after checkout + (extending, ending early) is managed in your own Stripe account via + `stripe.subscription_id`. + + A subscription request you cancel (or that expires unpaid) cancels the + incomplete Stripe subscription — nothing lingers on your account. + + ## Pre-created customers + + By default each request stands alone: payment mode attaches no Customer, + and subscription mode creates a fresh one. If you already manage + Customers on your connected account, pass their id as `customer_id` + (`cus_...`) on create — in payment mode the charge lands on that + customer's payment history, and in subscription mode the subscription is + created on them instead of on a new Customer. The id must reference an + existing, non-deleted customer on your connected account or the request + fails with `400`. We never modify a customer you pass — no metadata is + stamped on it. + + ## Sending the link + + Deliver the `checkout_url` as a **`link` message part** via + `POST /v3/chats/{chatId}/messages` — it renders as a rich card with your + branding (title, amount, image) instead of a bare URL, which converts far + better. A `link` part must be the only part in the message. See + [Rich Link Previews](/guides/messaging/sending-messages). + + On a supported iPhone the link opens an **Apple Pay App Clip** — a native, + no-install checkout sheet. Everywhere else (Android, desktop, iPhones + without the App Clip yet) the same URL opens the web checkout, so the link + always works. The App Clip experience for your payment links is registered + automatically by Linq and refreshed whenever you update your Agent Pay + branding; a newly registered experience can take up to ~24 hours to + activate on Apple's side, during which links open the web checkout. + + ## Webhooks + + Subscribe to payment lifecycle events to reconcile server-side rather than + polling: `payment.succeeded`, `payment.canceled`, and `payment.expired`. + Each event carries the payment request id, amount, currency, and your + `metadata`. See [Webhooks](/guides/webhooks). + """ + from .resources.payment_requests import PaymentRequestsResource + + return PaymentRequestsResource(self) + @cached_property def webhook_events(self) -> WebhookEventsResource: """ @@ -1232,6 +1354,126 @@ def available_number(self) -> AsyncAvailableNumberResource: return AsyncAvailableNumberResource(self) + @cached_property + def payment_requests(self) -> AsyncPaymentRequestsResource: + """Request a payment from a recipient over iMessage. + + You create a payment + request, send its `checkout_url` to the recipient, and they pay with Apple + Pay or card. Funds settle **directly to your own Stripe account** — Linq + never holds the money. + + ## How it works + + 1. **Create** a payment request with an amount and currency. You get back a + `checkout_url` and a `status` of `requested`. + 2. **Send** the `checkout_url` to the recipient as a `link` message part so + it arrives as a tappable card (see *Sending the link* below). + 3. The recipient **pays** on the hosted checkout (Apple Pay App Clip on a + supported iPhone, web checkout everywhere else). + 4. You receive a **`payment.succeeded`** webhook and the request's `status` + becomes `succeeded`. Requests you don't collect eventually `expire`. + + ## Connected accounts (Stripe Standard, direct charges) + + Agent Pay runs on **Stripe Connect Standard accounts** using **direct + charges**: the charge is created on *your* connected account and **you are + the merchant of record**. That means the money, the payout schedule, the + customer relationship, and the compliance surface are all yours — Linq + orchestrates the request and the checkout but is never in the funds flow. + + **Refunds, disputes, and chargebacks are handled by you, in your own Stripe + Dashboard.** Because charges settle directly to your account, Linq has no + custody of the funds and cannot issue refunds or contest disputes on your + behalf — and there is no refund/dispute endpoint in this API by design. Use + the Stripe Dashboard (or the Stripe API on your own account) for the money + lifecycle after a payment succeeds. + + ## Getting set up + + Open **Agent Pay** in your Linq dashboard + (`https://zero.linqapp.com/organization/payments`), click **Connect Stripe**, + and complete Stripe's onboarding (business details + a bank account). When + your account reaches `charges_enabled`, request creation unlocks; until you + connect Stripe, `POST /v3/payment_requests` returns `403`. You can keep + collecting even while Stripe finishes background verification. + + ## Subscriptions + + Set `mode: subscription` on `POST /v3/payment_requests` to start an + **auto-renewing subscription** instead of a one-time charge. Instead of an + amount, you pass a `price_id` — an active **recurring Price** on your + connected Stripe account (create one in your Stripe Dashboard under + Product catalog; if you sell through Stripe Payment Links today, reuse the + price your link is built from). The recipient pays the first invoice at + the same checkout, and their payment method is saved to the subscription + for automatic renewals. + + The division of labor is deliberate: **Linq handles the first payment, + your Stripe account handles the rest.** The request reaches `succeeded` + when the first invoice is paid; from then on the subscription lives + entirely on your connected account. The response's `stripe` object gives + you the join keys — `customer_id` and `subscription_id` — so renewals, + plan changes, dunning, and cancellation are managed with your own Stripe + Dashboard/API and your own Stripe webhooks. Your `metadata` is stamped on + the Customer and Subscription, so correlating in either direction is + trivial. There are no renewal webhooks from Linq by design. + + ### Free trials + + Add `trial_period_days` (or a fixed `trial_end` timestamp) to start the + subscription with a free trial. The checkout still collects the + recipient's payment method — the pay sheet shows "$0 due today" with the + first charge date — and saves it to the subscription; Stripe bills it + automatically when the trial ends. The request reaches `succeeded` when + the card is collected, and the response carries `trial_end`. If the trial + would end without a payment method on file, the subscription cancels + rather than generating unpayable invoices. Trial lifecycle after checkout + (extending, ending early) is managed in your own Stripe account via + `stripe.subscription_id`. + + A subscription request you cancel (or that expires unpaid) cancels the + incomplete Stripe subscription — nothing lingers on your account. + + ## Pre-created customers + + By default each request stands alone: payment mode attaches no Customer, + and subscription mode creates a fresh one. If you already manage + Customers on your connected account, pass their id as `customer_id` + (`cus_...`) on create — in payment mode the charge lands on that + customer's payment history, and in subscription mode the subscription is + created on them instead of on a new Customer. The id must reference an + existing, non-deleted customer on your connected account or the request + fails with `400`. We never modify a customer you pass — no metadata is + stamped on it. + + ## Sending the link + + Deliver the `checkout_url` as a **`link` message part** via + `POST /v3/chats/{chatId}/messages` — it renders as a rich card with your + branding (title, amount, image) instead of a bare URL, which converts far + better. A `link` part must be the only part in the message. See + [Rich Link Previews](/guides/messaging/sending-messages). + + On a supported iPhone the link opens an **Apple Pay App Clip** — a native, + no-install checkout sheet. Everywhere else (Android, desktop, iPhones + without the App Clip yet) the same URL opens the web checkout, so the link + always works. The App Clip experience for your payment links is registered + automatically by Linq and refreshed whenever you update your Agent Pay + branding; a newly registered experience can take up to ~24 hours to + activate on Apple's side, during which links open the web checkout. + + ## Webhooks + + Subscribe to payment lifecycle events to reconcile server-side rather than + polling: `payment.succeeded`, `payment.canceled`, and `payment.expired`. + Each event carries the payment request id, amount, currency, and your + `metadata`. See [Webhooks](/guides/webhooks). + """ + from .resources.payment_requests import AsyncPaymentRequestsResource + + return AsyncPaymentRequestsResource(self) + @cached_property def webhook_events(self) -> AsyncWebhookEventsResource: """ @@ -1955,6 +2197,126 @@ def available_number(self) -> available_number.AvailableNumberResourceWithRawRes return AvailableNumberResourceWithRawResponse(self._client.available_number) + @cached_property + def payment_requests(self) -> payment_requests.PaymentRequestsResourceWithRawResponse: + """Request a payment from a recipient over iMessage. + + You create a payment + request, send its `checkout_url` to the recipient, and they pay with Apple + Pay or card. Funds settle **directly to your own Stripe account** — Linq + never holds the money. + + ## How it works + + 1. **Create** a payment request with an amount and currency. You get back a + `checkout_url` and a `status` of `requested`. + 2. **Send** the `checkout_url` to the recipient as a `link` message part so + it arrives as a tappable card (see *Sending the link* below). + 3. The recipient **pays** on the hosted checkout (Apple Pay App Clip on a + supported iPhone, web checkout everywhere else). + 4. You receive a **`payment.succeeded`** webhook and the request's `status` + becomes `succeeded`. Requests you don't collect eventually `expire`. + + ## Connected accounts (Stripe Standard, direct charges) + + Agent Pay runs on **Stripe Connect Standard accounts** using **direct + charges**: the charge is created on *your* connected account and **you are + the merchant of record**. That means the money, the payout schedule, the + customer relationship, and the compliance surface are all yours — Linq + orchestrates the request and the checkout but is never in the funds flow. + + **Refunds, disputes, and chargebacks are handled by you, in your own Stripe + Dashboard.** Because charges settle directly to your account, Linq has no + custody of the funds and cannot issue refunds or contest disputes on your + behalf — and there is no refund/dispute endpoint in this API by design. Use + the Stripe Dashboard (or the Stripe API on your own account) for the money + lifecycle after a payment succeeds. + + ## Getting set up + + Open **Agent Pay** in your Linq dashboard + (`https://zero.linqapp.com/organization/payments`), click **Connect Stripe**, + and complete Stripe's onboarding (business details + a bank account). When + your account reaches `charges_enabled`, request creation unlocks; until you + connect Stripe, `POST /v3/payment_requests` returns `403`. You can keep + collecting even while Stripe finishes background verification. + + ## Subscriptions + + Set `mode: subscription` on `POST /v3/payment_requests` to start an + **auto-renewing subscription** instead of a one-time charge. Instead of an + amount, you pass a `price_id` — an active **recurring Price** on your + connected Stripe account (create one in your Stripe Dashboard under + Product catalog; if you sell through Stripe Payment Links today, reuse the + price your link is built from). The recipient pays the first invoice at + the same checkout, and their payment method is saved to the subscription + for automatic renewals. + + The division of labor is deliberate: **Linq handles the first payment, + your Stripe account handles the rest.** The request reaches `succeeded` + when the first invoice is paid; from then on the subscription lives + entirely on your connected account. The response's `stripe` object gives + you the join keys — `customer_id` and `subscription_id` — so renewals, + plan changes, dunning, and cancellation are managed with your own Stripe + Dashboard/API and your own Stripe webhooks. Your `metadata` is stamped on + the Customer and Subscription, so correlating in either direction is + trivial. There are no renewal webhooks from Linq by design. + + ### Free trials + + Add `trial_period_days` (or a fixed `trial_end` timestamp) to start the + subscription with a free trial. The checkout still collects the + recipient's payment method — the pay sheet shows "$0 due today" with the + first charge date — and saves it to the subscription; Stripe bills it + automatically when the trial ends. The request reaches `succeeded` when + the card is collected, and the response carries `trial_end`. If the trial + would end without a payment method on file, the subscription cancels + rather than generating unpayable invoices. Trial lifecycle after checkout + (extending, ending early) is managed in your own Stripe account via + `stripe.subscription_id`. + + A subscription request you cancel (or that expires unpaid) cancels the + incomplete Stripe subscription — nothing lingers on your account. + + ## Pre-created customers + + By default each request stands alone: payment mode attaches no Customer, + and subscription mode creates a fresh one. If you already manage + Customers on your connected account, pass their id as `customer_id` + (`cus_...`) on create — in payment mode the charge lands on that + customer's payment history, and in subscription mode the subscription is + created on them instead of on a new Customer. The id must reference an + existing, non-deleted customer on your connected account or the request + fails with `400`. We never modify a customer you pass — no metadata is + stamped on it. + + ## Sending the link + + Deliver the `checkout_url` as a **`link` message part** via + `POST /v3/chats/{chatId}/messages` — it renders as a rich card with your + branding (title, amount, image) instead of a bare URL, which converts far + better. A `link` part must be the only part in the message. See + [Rich Link Previews](/guides/messaging/sending-messages). + + On a supported iPhone the link opens an **Apple Pay App Clip** — a native, + no-install checkout sheet. Everywhere else (Android, desktop, iPhones + without the App Clip yet) the same URL opens the web checkout, so the link + always works. The App Clip experience for your payment links is registered + automatically by Linq and refreshed whenever you update your Agent Pay + branding; a newly registered experience can take up to ~24 hours to + activate on Apple's side, during which links open the web checkout. + + ## Webhooks + + Subscribe to payment lifecycle events to reconcile server-side rather than + polling: `payment.succeeded`, `payment.canceled`, and `payment.expired`. + Each event carries the payment request id, amount, currency, and your + `metadata`. See [Webhooks](/guides/webhooks). + """ + from .resources.payment_requests import PaymentRequestsResourceWithRawResponse + + return PaymentRequestsResourceWithRawResponse(self._client.payment_requests) + @cached_property def webhook_events(self) -> webhook_events.WebhookEventsResourceWithRawResponse: """ @@ -2551,6 +2913,126 @@ def available_number(self) -> available_number.AsyncAvailableNumberResourceWithR return AsyncAvailableNumberResourceWithRawResponse(self._client.available_number) + @cached_property + def payment_requests(self) -> payment_requests.AsyncPaymentRequestsResourceWithRawResponse: + """Request a payment from a recipient over iMessage. + + You create a payment + request, send its `checkout_url` to the recipient, and they pay with Apple + Pay or card. Funds settle **directly to your own Stripe account** — Linq + never holds the money. + + ## How it works + + 1. **Create** a payment request with an amount and currency. You get back a + `checkout_url` and a `status` of `requested`. + 2. **Send** the `checkout_url` to the recipient as a `link` message part so + it arrives as a tappable card (see *Sending the link* below). + 3. The recipient **pays** on the hosted checkout (Apple Pay App Clip on a + supported iPhone, web checkout everywhere else). + 4. You receive a **`payment.succeeded`** webhook and the request's `status` + becomes `succeeded`. Requests you don't collect eventually `expire`. + + ## Connected accounts (Stripe Standard, direct charges) + + Agent Pay runs on **Stripe Connect Standard accounts** using **direct + charges**: the charge is created on *your* connected account and **you are + the merchant of record**. That means the money, the payout schedule, the + customer relationship, and the compliance surface are all yours — Linq + orchestrates the request and the checkout but is never in the funds flow. + + **Refunds, disputes, and chargebacks are handled by you, in your own Stripe + Dashboard.** Because charges settle directly to your account, Linq has no + custody of the funds and cannot issue refunds or contest disputes on your + behalf — and there is no refund/dispute endpoint in this API by design. Use + the Stripe Dashboard (or the Stripe API on your own account) for the money + lifecycle after a payment succeeds. + + ## Getting set up + + Open **Agent Pay** in your Linq dashboard + (`https://zero.linqapp.com/organization/payments`), click **Connect Stripe**, + and complete Stripe's onboarding (business details + a bank account). When + your account reaches `charges_enabled`, request creation unlocks; until you + connect Stripe, `POST /v3/payment_requests` returns `403`. You can keep + collecting even while Stripe finishes background verification. + + ## Subscriptions + + Set `mode: subscription` on `POST /v3/payment_requests` to start an + **auto-renewing subscription** instead of a one-time charge. Instead of an + amount, you pass a `price_id` — an active **recurring Price** on your + connected Stripe account (create one in your Stripe Dashboard under + Product catalog; if you sell through Stripe Payment Links today, reuse the + price your link is built from). The recipient pays the first invoice at + the same checkout, and their payment method is saved to the subscription + for automatic renewals. + + The division of labor is deliberate: **Linq handles the first payment, + your Stripe account handles the rest.** The request reaches `succeeded` + when the first invoice is paid; from then on the subscription lives + entirely on your connected account. The response's `stripe` object gives + you the join keys — `customer_id` and `subscription_id` — so renewals, + plan changes, dunning, and cancellation are managed with your own Stripe + Dashboard/API and your own Stripe webhooks. Your `metadata` is stamped on + the Customer and Subscription, so correlating in either direction is + trivial. There are no renewal webhooks from Linq by design. + + ### Free trials + + Add `trial_period_days` (or a fixed `trial_end` timestamp) to start the + subscription with a free trial. The checkout still collects the + recipient's payment method — the pay sheet shows "$0 due today" with the + first charge date — and saves it to the subscription; Stripe bills it + automatically when the trial ends. The request reaches `succeeded` when + the card is collected, and the response carries `trial_end`. If the trial + would end without a payment method on file, the subscription cancels + rather than generating unpayable invoices. Trial lifecycle after checkout + (extending, ending early) is managed in your own Stripe account via + `stripe.subscription_id`. + + A subscription request you cancel (or that expires unpaid) cancels the + incomplete Stripe subscription — nothing lingers on your account. + + ## Pre-created customers + + By default each request stands alone: payment mode attaches no Customer, + and subscription mode creates a fresh one. If you already manage + Customers on your connected account, pass their id as `customer_id` + (`cus_...`) on create — in payment mode the charge lands on that + customer's payment history, and in subscription mode the subscription is + created on them instead of on a new Customer. The id must reference an + existing, non-deleted customer on your connected account or the request + fails with `400`. We never modify a customer you pass — no metadata is + stamped on it. + + ## Sending the link + + Deliver the `checkout_url` as a **`link` message part** via + `POST /v3/chats/{chatId}/messages` — it renders as a rich card with your + branding (title, amount, image) instead of a bare URL, which converts far + better. A `link` part must be the only part in the message. See + [Rich Link Previews](/guides/messaging/sending-messages). + + On a supported iPhone the link opens an **Apple Pay App Clip** — a native, + no-install checkout sheet. Everywhere else (Android, desktop, iPhones + without the App Clip yet) the same URL opens the web checkout, so the link + always works. The App Clip experience for your payment links is registered + automatically by Linq and refreshed whenever you update your Agent Pay + branding; a newly registered experience can take up to ~24 hours to + activate on Apple's side, during which links open the web checkout. + + ## Webhooks + + Subscribe to payment lifecycle events to reconcile server-side rather than + polling: `payment.succeeded`, `payment.canceled`, and `payment.expired`. + Each event carries the payment request id, amount, currency, and your + `metadata`. See [Webhooks](/guides/webhooks). + """ + from .resources.payment_requests import AsyncPaymentRequestsResourceWithRawResponse + + return AsyncPaymentRequestsResourceWithRawResponse(self._client.payment_requests) + @cached_property def webhook_events(self) -> webhook_events.AsyncWebhookEventsResourceWithRawResponse: """ @@ -3147,6 +3629,126 @@ def available_number(self) -> available_number.AvailableNumberResourceWithStream return AvailableNumberResourceWithStreamingResponse(self._client.available_number) + @cached_property + def payment_requests(self) -> payment_requests.PaymentRequestsResourceWithStreamingResponse: + """Request a payment from a recipient over iMessage. + + You create a payment + request, send its `checkout_url` to the recipient, and they pay with Apple + Pay or card. Funds settle **directly to your own Stripe account** — Linq + never holds the money. + + ## How it works + + 1. **Create** a payment request with an amount and currency. You get back a + `checkout_url` and a `status` of `requested`. + 2. **Send** the `checkout_url` to the recipient as a `link` message part so + it arrives as a tappable card (see *Sending the link* below). + 3. The recipient **pays** on the hosted checkout (Apple Pay App Clip on a + supported iPhone, web checkout everywhere else). + 4. You receive a **`payment.succeeded`** webhook and the request's `status` + becomes `succeeded`. Requests you don't collect eventually `expire`. + + ## Connected accounts (Stripe Standard, direct charges) + + Agent Pay runs on **Stripe Connect Standard accounts** using **direct + charges**: the charge is created on *your* connected account and **you are + the merchant of record**. That means the money, the payout schedule, the + customer relationship, and the compliance surface are all yours — Linq + orchestrates the request and the checkout but is never in the funds flow. + + **Refunds, disputes, and chargebacks are handled by you, in your own Stripe + Dashboard.** Because charges settle directly to your account, Linq has no + custody of the funds and cannot issue refunds or contest disputes on your + behalf — and there is no refund/dispute endpoint in this API by design. Use + the Stripe Dashboard (or the Stripe API on your own account) for the money + lifecycle after a payment succeeds. + + ## Getting set up + + Open **Agent Pay** in your Linq dashboard + (`https://zero.linqapp.com/organization/payments`), click **Connect Stripe**, + and complete Stripe's onboarding (business details + a bank account). When + your account reaches `charges_enabled`, request creation unlocks; until you + connect Stripe, `POST /v3/payment_requests` returns `403`. You can keep + collecting even while Stripe finishes background verification. + + ## Subscriptions + + Set `mode: subscription` on `POST /v3/payment_requests` to start an + **auto-renewing subscription** instead of a one-time charge. Instead of an + amount, you pass a `price_id` — an active **recurring Price** on your + connected Stripe account (create one in your Stripe Dashboard under + Product catalog; if you sell through Stripe Payment Links today, reuse the + price your link is built from). The recipient pays the first invoice at + the same checkout, and their payment method is saved to the subscription + for automatic renewals. + + The division of labor is deliberate: **Linq handles the first payment, + your Stripe account handles the rest.** The request reaches `succeeded` + when the first invoice is paid; from then on the subscription lives + entirely on your connected account. The response's `stripe` object gives + you the join keys — `customer_id` and `subscription_id` — so renewals, + plan changes, dunning, and cancellation are managed with your own Stripe + Dashboard/API and your own Stripe webhooks. Your `metadata` is stamped on + the Customer and Subscription, so correlating in either direction is + trivial. There are no renewal webhooks from Linq by design. + + ### Free trials + + Add `trial_period_days` (or a fixed `trial_end` timestamp) to start the + subscription with a free trial. The checkout still collects the + recipient's payment method — the pay sheet shows "$0 due today" with the + first charge date — and saves it to the subscription; Stripe bills it + automatically when the trial ends. The request reaches `succeeded` when + the card is collected, and the response carries `trial_end`. If the trial + would end without a payment method on file, the subscription cancels + rather than generating unpayable invoices. Trial lifecycle after checkout + (extending, ending early) is managed in your own Stripe account via + `stripe.subscription_id`. + + A subscription request you cancel (or that expires unpaid) cancels the + incomplete Stripe subscription — nothing lingers on your account. + + ## Pre-created customers + + By default each request stands alone: payment mode attaches no Customer, + and subscription mode creates a fresh one. If you already manage + Customers on your connected account, pass their id as `customer_id` + (`cus_...`) on create — in payment mode the charge lands on that + customer's payment history, and in subscription mode the subscription is + created on them instead of on a new Customer. The id must reference an + existing, non-deleted customer on your connected account or the request + fails with `400`. We never modify a customer you pass — no metadata is + stamped on it. + + ## Sending the link + + Deliver the `checkout_url` as a **`link` message part** via + `POST /v3/chats/{chatId}/messages` — it renders as a rich card with your + branding (title, amount, image) instead of a bare URL, which converts far + better. A `link` part must be the only part in the message. See + [Rich Link Previews](/guides/messaging/sending-messages). + + On a supported iPhone the link opens an **Apple Pay App Clip** — a native, + no-install checkout sheet. Everywhere else (Android, desktop, iPhones + without the App Clip yet) the same URL opens the web checkout, so the link + always works. The App Clip experience for your payment links is registered + automatically by Linq and refreshed whenever you update your Agent Pay + branding; a newly registered experience can take up to ~24 hours to + activate on Apple's side, during which links open the web checkout. + + ## Webhooks + + Subscribe to payment lifecycle events to reconcile server-side rather than + polling: `payment.succeeded`, `payment.canceled`, and `payment.expired`. + Each event carries the payment request id, amount, currency, and your + `metadata`. See [Webhooks](/guides/webhooks). + """ + from .resources.payment_requests import PaymentRequestsResourceWithStreamingResponse + + return PaymentRequestsResourceWithStreamingResponse(self._client.payment_requests) + @cached_property def webhook_events(self) -> webhook_events.WebhookEventsResourceWithStreamingResponse: """ @@ -3743,6 +4345,126 @@ def available_number(self) -> available_number.AsyncAvailableNumberResourceWithS return AsyncAvailableNumberResourceWithStreamingResponse(self._client.available_number) + @cached_property + def payment_requests(self) -> payment_requests.AsyncPaymentRequestsResourceWithStreamingResponse: + """Request a payment from a recipient over iMessage. + + You create a payment + request, send its `checkout_url` to the recipient, and they pay with Apple + Pay or card. Funds settle **directly to your own Stripe account** — Linq + never holds the money. + + ## How it works + + 1. **Create** a payment request with an amount and currency. You get back a + `checkout_url` and a `status` of `requested`. + 2. **Send** the `checkout_url` to the recipient as a `link` message part so + it arrives as a tappable card (see *Sending the link* below). + 3. The recipient **pays** on the hosted checkout (Apple Pay App Clip on a + supported iPhone, web checkout everywhere else). + 4. You receive a **`payment.succeeded`** webhook and the request's `status` + becomes `succeeded`. Requests you don't collect eventually `expire`. + + ## Connected accounts (Stripe Standard, direct charges) + + Agent Pay runs on **Stripe Connect Standard accounts** using **direct + charges**: the charge is created on *your* connected account and **you are + the merchant of record**. That means the money, the payout schedule, the + customer relationship, and the compliance surface are all yours — Linq + orchestrates the request and the checkout but is never in the funds flow. + + **Refunds, disputes, and chargebacks are handled by you, in your own Stripe + Dashboard.** Because charges settle directly to your account, Linq has no + custody of the funds and cannot issue refunds or contest disputes on your + behalf — and there is no refund/dispute endpoint in this API by design. Use + the Stripe Dashboard (or the Stripe API on your own account) for the money + lifecycle after a payment succeeds. + + ## Getting set up + + Open **Agent Pay** in your Linq dashboard + (`https://zero.linqapp.com/organization/payments`), click **Connect Stripe**, + and complete Stripe's onboarding (business details + a bank account). When + your account reaches `charges_enabled`, request creation unlocks; until you + connect Stripe, `POST /v3/payment_requests` returns `403`. You can keep + collecting even while Stripe finishes background verification. + + ## Subscriptions + + Set `mode: subscription` on `POST /v3/payment_requests` to start an + **auto-renewing subscription** instead of a one-time charge. Instead of an + amount, you pass a `price_id` — an active **recurring Price** on your + connected Stripe account (create one in your Stripe Dashboard under + Product catalog; if you sell through Stripe Payment Links today, reuse the + price your link is built from). The recipient pays the first invoice at + the same checkout, and their payment method is saved to the subscription + for automatic renewals. + + The division of labor is deliberate: **Linq handles the first payment, + your Stripe account handles the rest.** The request reaches `succeeded` + when the first invoice is paid; from then on the subscription lives + entirely on your connected account. The response's `stripe` object gives + you the join keys — `customer_id` and `subscription_id` — so renewals, + plan changes, dunning, and cancellation are managed with your own Stripe + Dashboard/API and your own Stripe webhooks. Your `metadata` is stamped on + the Customer and Subscription, so correlating in either direction is + trivial. There are no renewal webhooks from Linq by design. + + ### Free trials + + Add `trial_period_days` (or a fixed `trial_end` timestamp) to start the + subscription with a free trial. The checkout still collects the + recipient's payment method — the pay sheet shows "$0 due today" with the + first charge date — and saves it to the subscription; Stripe bills it + automatically when the trial ends. The request reaches `succeeded` when + the card is collected, and the response carries `trial_end`. If the trial + would end without a payment method on file, the subscription cancels + rather than generating unpayable invoices. Trial lifecycle after checkout + (extending, ending early) is managed in your own Stripe account via + `stripe.subscription_id`. + + A subscription request you cancel (or that expires unpaid) cancels the + incomplete Stripe subscription — nothing lingers on your account. + + ## Pre-created customers + + By default each request stands alone: payment mode attaches no Customer, + and subscription mode creates a fresh one. If you already manage + Customers on your connected account, pass their id as `customer_id` + (`cus_...`) on create — in payment mode the charge lands on that + customer's payment history, and in subscription mode the subscription is + created on them instead of on a new Customer. The id must reference an + existing, non-deleted customer on your connected account or the request + fails with `400`. We never modify a customer you pass — no metadata is + stamped on it. + + ## Sending the link + + Deliver the `checkout_url` as a **`link` message part** via + `POST /v3/chats/{chatId}/messages` — it renders as a rich card with your + branding (title, amount, image) instead of a bare URL, which converts far + better. A `link` part must be the only part in the message. See + [Rich Link Previews](/guides/messaging/sending-messages). + + On a supported iPhone the link opens an **Apple Pay App Clip** — a native, + no-install checkout sheet. Everywhere else (Android, desktop, iPhones + without the App Clip yet) the same URL opens the web checkout, so the link + always works. The App Clip experience for your payment links is registered + automatically by Linq and refreshed whenever you update your Agent Pay + branding; a newly registered experience can take up to ~24 hours to + activate on Apple's side, during which links open the web checkout. + + ## Webhooks + + Subscribe to payment lifecycle events to reconcile server-side rather than + polling: `payment.succeeded`, `payment.canceled`, and `payment.expired`. + Each event carries the payment request id, amount, currency, and your + `metadata`. See [Webhooks](/guides/webhooks). + """ + from .resources.payment_requests import AsyncPaymentRequestsResourceWithStreamingResponse + + return AsyncPaymentRequestsResourceWithStreamingResponse(self._client.payment_requests) + @cached_property def webhook_events(self) -> webhook_events.AsyncWebhookEventsResourceWithStreamingResponse: """ diff --git a/src/linq/resources/__init__.py b/src/linq/resources/__init__.py index 00e6eb8..b0ae102 100644 --- a/src/linq/resources/__init__.py +++ b/src/linq/resources/__init__.py @@ -73,6 +73,14 @@ AvailableNumberResourceWithStreamingResponse, AsyncAvailableNumberResourceWithStreamingResponse, ) +from .payment_requests import ( + PaymentRequestsResource, + AsyncPaymentRequestsResource, + PaymentRequestsResourceWithRawResponse, + AsyncPaymentRequestsResourceWithRawResponse, + PaymentRequestsResourceWithStreamingResponse, + AsyncPaymentRequestsResourceWithStreamingResponse, +) from .webhook_subscriptions import ( WebhookSubscriptionsResource, AsyncWebhookSubscriptionsResource, @@ -119,6 +127,12 @@ "AsyncAvailableNumberResourceWithRawResponse", "AvailableNumberResourceWithStreamingResponse", "AsyncAvailableNumberResourceWithStreamingResponse", + "PaymentRequestsResource", + "AsyncPaymentRequestsResource", + "PaymentRequestsResourceWithRawResponse", + "AsyncPaymentRequestsResourceWithRawResponse", + "PaymentRequestsResourceWithStreamingResponse", + "AsyncPaymentRequestsResourceWithStreamingResponse", "WebhookEventsResource", "AsyncWebhookEventsResource", "WebhookEventsResourceWithRawResponse", diff --git a/src/linq/resources/available_number.py b/src/linq/resources/available_number.py index 0fb879a..fe2ff48 100644 --- a/src/linq/resources/available_number.py +++ b/src/linq/resources/available_number.py @@ -67,9 +67,11 @@ def retrieve( reusing the line an existing chat with those recipients is already on. Without `to`, the best healthy line is chosen. - This is advisory: it does not reserve the line or change selection state. Pass - the returned `phone_number` as `from` when you create the chat to guarantee the - same line. + This does not reserve the line. Without `to`, the least-recently-used healthy + line is returned — suggestions and your own sends (including an explicit `from` + on chat creation) both count as use, so successive calls cycle through your + healthy lines and traffic spreads evenly. Pass the returned `phone_number` as + `from` when you create the chat to guarantee the same line. Also returns `vcf_url`: a time-limited link to a vCard (`.vcf`) for the chosen line, carrying its contact card (name/photo) with the chosen number as the @@ -148,9 +150,11 @@ async def retrieve( reusing the line an existing chat with those recipients is already on. Without `to`, the best healthy line is chosen. - This is advisory: it does not reserve the line or change selection state. Pass - the returned `phone_number` as `from` when you create the chat to guarantee the - same line. + This does not reserve the line. Without `to`, the least-recently-used healthy + line is returned — suggestions and your own sends (including an explicit `from` + on chat creation) both count as use, so successive calls cycle through your + healthy lines and traffic spreads evenly. Pass the returned `phone_number` as + `from` when you create the chat to guarantee the same line. Also returns `vcf_url`: a time-limited link to a vCard (`.vcf`) for the chosen line, carrying its contact card (name/photo) with the chosen number as the diff --git a/src/linq/resources/chats/chats.py b/src/linq/resources/chats/chats.py index 20022f4..81edb5e 100644 --- a/src/linq/resources/chats/chats.py +++ b/src/linq/resources/chats/chats.py @@ -172,14 +172,43 @@ def messages(self) -> MessagesResource: @cached_property def location(self) -> LocationResource: - """Request and retrieve real-time location data via iMessage. - - Use these endpoints to request a contact's location, retrieve location data - for contacts who are sharing with you, and subscribe to webhooks when someone - starts or stops sharing their location. + """ + Request a contact's location, retrieve location for contacts sharing with you, + and subscribe to webhooks when someone starts or stops sharing. **Coordinates** are returned in [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) format: `[longitude, latitude]` or `[longitude, latitude, altitude]` if altitude is available. + + ### Reading location is poll-based + + Poll `GET /v3/chats/{chatId}/location` whenever you need the latest position. + **There is no webhook that pushes updated coordinates** — the + `location.sharing.started` / `location.sharing.stopped` webhooks fire only when a + contact begins or ends sharing, not on each position update. To track a moving + contact, poll the `GET` endpoint. + + ### Freshness + + Each feature's `properties.updated_at` tells you when that participant's + location was last updated — use it to judge freshness. + + ### Polling guidance + + Locations refresh on Apple's cadence, not per request — polling faster than a + participant's location actually updates just returns the same position. Poll at a + modest interval (for example, once every few minutes per chat) rather than + continuously. + + ### Why is location empty after `location.sharing.started` fired? + + If the contact started sharing from the **standalone Find My app** instead of the + Messages conversation, the share may be tied to their **Apple ID email** rather + than their phone number — the webhook's `shared_by` field shows the email in that + case. Location is readable only through a chat with the handle that shared, so + `GET /v3/chats/{chatId}/location` on the phone-number chat stays empty. + + The fix: have the contact stop sharing and re-share from **Find My inside the + Messages conversation** with your number. """ return LocationResource(self._client) @@ -764,14 +793,43 @@ def messages(self) -> AsyncMessagesResource: @cached_property def location(self) -> AsyncLocationResource: - """Request and retrieve real-time location data via iMessage. - - Use these endpoints to request a contact's location, retrieve location data - for contacts who are sharing with you, and subscribe to webhooks when someone - starts or stops sharing their location. + """ + Request a contact's location, retrieve location for contacts sharing with you, + and subscribe to webhooks when someone starts or stops sharing. **Coordinates** are returned in [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) format: `[longitude, latitude]` or `[longitude, latitude, altitude]` if altitude is available. + + ### Reading location is poll-based + + Poll `GET /v3/chats/{chatId}/location` whenever you need the latest position. + **There is no webhook that pushes updated coordinates** — the + `location.sharing.started` / `location.sharing.stopped` webhooks fire only when a + contact begins or ends sharing, not on each position update. To track a moving + contact, poll the `GET` endpoint. + + ### Freshness + + Each feature's `properties.updated_at` tells you when that participant's + location was last updated — use it to judge freshness. + + ### Polling guidance + + Locations refresh on Apple's cadence, not per request — polling faster than a + participant's location actually updates just returns the same position. Poll at a + modest interval (for example, once every few minutes per chat) rather than + continuously. + + ### Why is location empty after `location.sharing.started` fired? + + If the contact started sharing from the **standalone Find My app** instead of the + Messages conversation, the share may be tied to their **Apple ID email** rather + than their phone number — the webhook's `shared_by` field shows the email in that + case. Location is readable only through a chat with the handle that shared, so + `GET /v3/chats/{chatId}/location` on the phone-number chat stays empty. + + The fix: have the contact stop sharing and re-share from **Find My inside the + Messages conversation** with your number. """ return AsyncLocationResource(self._client) @@ -1384,14 +1442,43 @@ def messages(self) -> MessagesResourceWithRawResponse: @cached_property def location(self) -> LocationResourceWithRawResponse: - """Request and retrieve real-time location data via iMessage. - - Use these endpoints to request a contact's location, retrieve location data - for contacts who are sharing with you, and subscribe to webhooks when someone - starts or stops sharing their location. + """ + Request a contact's location, retrieve location for contacts sharing with you, + and subscribe to webhooks when someone starts or stops sharing. **Coordinates** are returned in [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) format: `[longitude, latitude]` or `[longitude, latitude, altitude]` if altitude is available. + + ### Reading location is poll-based + + Poll `GET /v3/chats/{chatId}/location` whenever you need the latest position. + **There is no webhook that pushes updated coordinates** — the + `location.sharing.started` / `location.sharing.stopped` webhooks fire only when a + contact begins or ends sharing, not on each position update. To track a moving + contact, poll the `GET` endpoint. + + ### Freshness + + Each feature's `properties.updated_at` tells you when that participant's + location was last updated — use it to judge freshness. + + ### Polling guidance + + Locations refresh on Apple's cadence, not per request — polling faster than a + participant's location actually updates just returns the same position. Poll at a + modest interval (for example, once every few minutes per chat) rather than + continuously. + + ### Why is location empty after `location.sharing.started` fired? + + If the contact started sharing from the **standalone Find My app** instead of the + Messages conversation, the share may be tied to their **Apple ID email** rather + than their phone number — the webhook's `shared_by` field shows the email in that + case. Location is readable only through a chat with the handle that shared, so + `GET /v3/chats/{chatId}/location` on the phone-number chat stays empty. + + The fix: have the contact stop sharing and re-share from **Find My inside the + Messages conversation** with your number. """ return LocationResourceWithRawResponse(self._chats.location) @@ -1532,14 +1619,43 @@ def messages(self) -> AsyncMessagesResourceWithRawResponse: @cached_property def location(self) -> AsyncLocationResourceWithRawResponse: - """Request and retrieve real-time location data via iMessage. - - Use these endpoints to request a contact's location, retrieve location data - for contacts who are sharing with you, and subscribe to webhooks when someone - starts or stops sharing their location. + """ + Request a contact's location, retrieve location for contacts sharing with you, + and subscribe to webhooks when someone starts or stops sharing. **Coordinates** are returned in [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) format: `[longitude, latitude]` or `[longitude, latitude, altitude]` if altitude is available. + + ### Reading location is poll-based + + Poll `GET /v3/chats/{chatId}/location` whenever you need the latest position. + **There is no webhook that pushes updated coordinates** — the + `location.sharing.started` / `location.sharing.stopped` webhooks fire only when a + contact begins or ends sharing, not on each position update. To track a moving + contact, poll the `GET` endpoint. + + ### Freshness + + Each feature's `properties.updated_at` tells you when that participant's + location was last updated — use it to judge freshness. + + ### Polling guidance + + Locations refresh on Apple's cadence, not per request — polling faster than a + participant's location actually updates just returns the same position. Poll at a + modest interval (for example, once every few minutes per chat) rather than + continuously. + + ### Why is location empty after `location.sharing.started` fired? + + If the contact started sharing from the **standalone Find My app** instead of the + Messages conversation, the share may be tied to their **Apple ID email** rather + than their phone number — the webhook's `shared_by` field shows the email in that + case. Location is readable only through a chat with the handle that shared, so + `GET /v3/chats/{chatId}/location` on the phone-number chat stays empty. + + The fix: have the contact stop sharing and re-share from **Find My inside the + Messages conversation** with your number. """ return AsyncLocationResourceWithRawResponse(self._chats.location) @@ -1680,14 +1796,43 @@ def messages(self) -> MessagesResourceWithStreamingResponse: @cached_property def location(self) -> LocationResourceWithStreamingResponse: - """Request and retrieve real-time location data via iMessage. - - Use these endpoints to request a contact's location, retrieve location data - for contacts who are sharing with you, and subscribe to webhooks when someone - starts or stops sharing their location. + """ + Request a contact's location, retrieve location for contacts sharing with you, + and subscribe to webhooks when someone starts or stops sharing. **Coordinates** are returned in [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) format: `[longitude, latitude]` or `[longitude, latitude, altitude]` if altitude is available. + + ### Reading location is poll-based + + Poll `GET /v3/chats/{chatId}/location` whenever you need the latest position. + **There is no webhook that pushes updated coordinates** — the + `location.sharing.started` / `location.sharing.stopped` webhooks fire only when a + contact begins or ends sharing, not on each position update. To track a moving + contact, poll the `GET` endpoint. + + ### Freshness + + Each feature's `properties.updated_at` tells you when that participant's + location was last updated — use it to judge freshness. + + ### Polling guidance + + Locations refresh on Apple's cadence, not per request — polling faster than a + participant's location actually updates just returns the same position. Poll at a + modest interval (for example, once every few minutes per chat) rather than + continuously. + + ### Why is location empty after `location.sharing.started` fired? + + If the contact started sharing from the **standalone Find My app** instead of the + Messages conversation, the share may be tied to their **Apple ID email** rather + than their phone number — the webhook's `shared_by` field shows the email in that + case. Location is readable only through a chat with the handle that shared, so + `GET /v3/chats/{chatId}/location` on the phone-number chat stays empty. + + The fix: have the contact stop sharing and re-share from **Find My inside the + Messages conversation** with your number. """ return LocationResourceWithStreamingResponse(self._chats.location) @@ -1828,13 +1973,42 @@ def messages(self) -> AsyncMessagesResourceWithStreamingResponse: @cached_property def location(self) -> AsyncLocationResourceWithStreamingResponse: - """Request and retrieve real-time location data via iMessage. - - Use these endpoints to request a contact's location, retrieve location data - for contacts who are sharing with you, and subscribe to webhooks when someone - starts or stops sharing their location. + """ + Request a contact's location, retrieve location for contacts sharing with you, + and subscribe to webhooks when someone starts or stops sharing. **Coordinates** are returned in [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) format: `[longitude, latitude]` or `[longitude, latitude, altitude]` if altitude is available. + + ### Reading location is poll-based + + Poll `GET /v3/chats/{chatId}/location` whenever you need the latest position. + **There is no webhook that pushes updated coordinates** — the + `location.sharing.started` / `location.sharing.stopped` webhooks fire only when a + contact begins or ends sharing, not on each position update. To track a moving + contact, poll the `GET` endpoint. + + ### Freshness + + Each feature's `properties.updated_at` tells you when that participant's + location was last updated — use it to judge freshness. + + ### Polling guidance + + Locations refresh on Apple's cadence, not per request — polling faster than a + participant's location actually updates just returns the same position. Poll at a + modest interval (for example, once every few minutes per chat) rather than + continuously. + + ### Why is location empty after `location.sharing.started` fired? + + If the contact started sharing from the **standalone Find My app** instead of the + Messages conversation, the share may be tied to their **Apple ID email** rather + than their phone number — the webhook's `shared_by` field shows the email in that + case. Location is readable only through a chat with the handle that shared, so + `GET /v3/chats/{chatId}/location` on the phone-number chat stays empty. + + The fix: have the contact stop sharing and re-share from **Find My inside the + Messages conversation** with your number. """ return AsyncLocationResourceWithStreamingResponse(self._chats.location) diff --git a/src/linq/resources/chats/location.py b/src/linq/resources/chats/location.py index d2dc409..9c9dd4b 100644 --- a/src/linq/resources/chats/location.py +++ b/src/linq/resources/chats/location.py @@ -22,14 +22,43 @@ class LocationResource(SyncAPIResource): - """Request and retrieve real-time location data via iMessage. - - Use these endpoints to request a contact's location, retrieve location data - for contacts who are sharing with you, and subscribe to webhooks when someone - starts or stops sharing their location. + """ + Request a contact's location, retrieve location for contacts sharing with you, + and subscribe to webhooks when someone starts or stops sharing. **Coordinates** are returned in [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) format: `[longitude, latitude]` or `[longitude, latitude, altitude]` if altitude is available. + + ### Reading location is poll-based + + Poll `GET /v3/chats/{chatId}/location` whenever you need the latest position. + **There is no webhook that pushes updated coordinates** — the + `location.sharing.started` / `location.sharing.stopped` webhooks fire only when a + contact begins or ends sharing, not on each position update. To track a moving + contact, poll the `GET` endpoint. + + ### Freshness + + Each feature's `properties.updated_at` tells you when that participant's + location was last updated — use it to judge freshness. + + ### Polling guidance + + Locations refresh on Apple's cadence, not per request — polling faster than a + participant's location actually updates just returns the same position. Poll at a + modest interval (for example, once every few minutes per chat) rather than + continuously. + + ### Why is location empty after `location.sharing.started` fired? + + If the contact started sharing from the **standalone Find My app** instead of the + Messages conversation, the share may be tied to their **Apple ID email** rather + than their phone number — the webhook's `shared_by` field shows the email in that + case. Location is readable only through a chat with the handle that shared, so + `GET /v3/chats/{chatId}/location` on the phone-number chat stays empty. + + The fix: have the contact stop sharing and re-share from **Find My inside the + Messages conversation** with your number. """ @cached_property @@ -75,7 +104,12 @@ def retrieve( `properties.handle` identifies the user. Returns an empty `data.features` array if no one is sharing or no location data - is available yet. + is available yet. If sharing started but this stays empty, see the **Location + Sharing** overview. + + Poll this endpoint to track a moving contact. `properties.updated_at` reflects + when each participant's location was last updated. There is no coordinate-update + webhook. See the **Location Sharing** overview for polling guidance. Args: extra_headers: Send extra headers @@ -108,14 +142,22 @@ def request( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> LocationRequestResponse: - """Send a location sharing request to a contact. + """Request a contact in a chat to share their location. + + They receive an iMessage + prompt and must accept before any location is available; once they do, read + their location coordinates with `GET /v3/chats/{chatId}/location`. + + The request is delivered asynchronously. The endpoint returns immediately with + `{ "success": true, "message": "Location request sent" }` and does not return + coordinates. - They will receive an iMessage - prompt asking them to share their location. + Location requests only work in **1:1 iMessage chats** (Apple limitation): - Location requests only work in **1:1 iMessage chats** (Apple limitation). - Attempting to request location in a group chat, or in an SMS or RCS chat, - returns `409` (Operation not supported on this chat's service type). + - Group chats (any service) return `409` with code `2016` + (`GroupChatNotSupported`). + - 1:1 SMS and RCS chats return `409` with code `2017` + (`ChatServiceNotSupported`). Args: extra_headers: Send extra headers @@ -138,14 +180,43 @@ def request( class AsyncLocationResource(AsyncAPIResource): - """Request and retrieve real-time location data via iMessage. - - Use these endpoints to request a contact's location, retrieve location data - for contacts who are sharing with you, and subscribe to webhooks when someone - starts or stops sharing their location. + """ + Request a contact's location, retrieve location for contacts sharing with you, + and subscribe to webhooks when someone starts or stops sharing. **Coordinates** are returned in [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) format: `[longitude, latitude]` or `[longitude, latitude, altitude]` if altitude is available. + + ### Reading location is poll-based + + Poll `GET /v3/chats/{chatId}/location` whenever you need the latest position. + **There is no webhook that pushes updated coordinates** — the + `location.sharing.started` / `location.sharing.stopped` webhooks fire only when a + contact begins or ends sharing, not on each position update. To track a moving + contact, poll the `GET` endpoint. + + ### Freshness + + Each feature's `properties.updated_at` tells you when that participant's + location was last updated — use it to judge freshness. + + ### Polling guidance + + Locations refresh on Apple's cadence, not per request — polling faster than a + participant's location actually updates just returns the same position. Poll at a + modest interval (for example, once every few minutes per chat) rather than + continuously. + + ### Why is location empty after `location.sharing.started` fired? + + If the contact started sharing from the **standalone Find My app** instead of the + Messages conversation, the share may be tied to their **Apple ID email** rather + than their phone number — the webhook's `shared_by` field shows the email in that + case. Location is readable only through a chat with the handle that shared, so + `GET /v3/chats/{chatId}/location` on the phone-number chat stays empty. + + The fix: have the contact stop sharing and re-share from **Find My inside the + Messages conversation** with your number. """ @cached_property @@ -191,7 +262,12 @@ async def retrieve( `properties.handle` identifies the user. Returns an empty `data.features` array if no one is sharing or no location data - is available yet. + is available yet. If sharing started but this stays empty, see the **Location + Sharing** overview. + + Poll this endpoint to track a moving contact. `properties.updated_at` reflects + when each participant's location was last updated. There is no coordinate-update + webhook. See the **Location Sharing** overview for polling guidance. Args: extra_headers: Send extra headers @@ -224,14 +300,22 @@ async def request( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> LocationRequestResponse: - """Send a location sharing request to a contact. + """Request a contact in a chat to share their location. + + They receive an iMessage + prompt and must accept before any location is available; once they do, read + their location coordinates with `GET /v3/chats/{chatId}/location`. + + The request is delivered asynchronously. The endpoint returns immediately with + `{ "success": true, "message": "Location request sent" }` and does not return + coordinates. - They will receive an iMessage - prompt asking them to share their location. + Location requests only work in **1:1 iMessage chats** (Apple limitation): - Location requests only work in **1:1 iMessage chats** (Apple limitation). - Attempting to request location in a group chat, or in an SMS or RCS chat, - returns `409` (Operation not supported on this chat's service type). + - Group chats (any service) return `409` with code `2016` + (`GroupChatNotSupported`). + - 1:1 SMS and RCS chats return `409` with code `2017` + (`ChatServiceNotSupported`). Args: extra_headers: Send extra headers diff --git a/src/linq/resources/messages.py b/src/linq/resources/messages.py index f2490ab..5078412 100644 --- a/src/linq/resources/messages.py +++ b/src/linq/resources/messages.py @@ -141,12 +141,14 @@ def create( ## How the from-number and chat are chosen - - **Reuse** — if a chat with exactly these recipients already exists and the - line it lives on is healthy, the message is sent into that chat on its - existing line (`from_selection.reason = reused_active_chat`). + - **Reuse** — if a chat with exactly these recipients already exists on a line + that can still send, the message is sent into that chat on its existing line + (`from_selection.reason = reused_active_chat`). The most-recently-active such + chat wins; chats stranded on flagged lines (e.g. by an earlier failover) are + skipped. - **New** — if no such chat exists, a new chat is created on the best available line (`from_selection.reason = new_best_number`). - - **Failover** — if a matching chat exists but its line has been flagged, a + - **Failover** — if matching chats exist but none is on a line that can send, a **new** chat is created on a fresh best line and the flagged chat is abandoned (`from_selection.reason = failover_flagged`, `previous_chat_id` set). If you supply `continuation_message`, that text is sent as the single message INSTEAD @@ -667,12 +669,14 @@ async def create( ## How the from-number and chat are chosen - - **Reuse** — if a chat with exactly these recipients already exists and the - line it lives on is healthy, the message is sent into that chat on its - existing line (`from_selection.reason = reused_active_chat`). + - **Reuse** — if a chat with exactly these recipients already exists on a line + that can still send, the message is sent into that chat on its existing line + (`from_selection.reason = reused_active_chat`). The most-recently-active such + chat wins; chats stranded on flagged lines (e.g. by an earlier failover) are + skipped. - **New** — if no such chat exists, a new chat is created on the best available line (`from_selection.reason = new_best_number`). - - **Failover** — if a matching chat exists but its line has been flagged, a + - **Failover** — if matching chats exist but none is on a line that can send, a **new** chat is created on a fresh best line and the flagged chat is abandoned (`from_selection.reason = failover_flagged`, `previous_chat_id` set). If you supply `continuation_message`, that text is sent as the single message INSTEAD diff --git a/src/linq/resources/payment_requests.py b/src/linq/resources/payment_requests.py new file mode 100644 index 0000000..537a5af --- /dev/null +++ b/src/linq/resources/payment_requests.py @@ -0,0 +1,838 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Union +from datetime import datetime +from typing_extensions import Literal + +import httpx + +from ..types import payment_request_list_params, payment_request_create_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.payment_request import PaymentRequest +from ..types.payment_request_list_response import PaymentRequestListResponse + +__all__ = ["PaymentRequestsResource", "AsyncPaymentRequestsResource"] + + +class PaymentRequestsResource(SyncAPIResource): + """Request a payment from a recipient over iMessage. + + You create a payment + request, send its `checkout_url` to the recipient, and they pay with Apple + Pay or card. Funds settle **directly to your own Stripe account** — Linq + never holds the money. + + ## How it works + + 1. **Create** a payment request with an amount and currency. You get back a + `checkout_url` and a `status` of `requested`. + 2. **Send** the `checkout_url` to the recipient as a `link` message part so + it arrives as a tappable card (see *Sending the link* below). + 3. The recipient **pays** on the hosted checkout (Apple Pay App Clip on a + supported iPhone, web checkout everywhere else). + 4. You receive a **`payment.succeeded`** webhook and the request's `status` + becomes `succeeded`. Requests you don't collect eventually `expire`. + + ## Connected accounts (Stripe Standard, direct charges) + + Agent Pay runs on **Stripe Connect Standard accounts** using **direct + charges**: the charge is created on *your* connected account and **you are + the merchant of record**. That means the money, the payout schedule, the + customer relationship, and the compliance surface are all yours — Linq + orchestrates the request and the checkout but is never in the funds flow. + + **Refunds, disputes, and chargebacks are handled by you, in your own Stripe + Dashboard.** Because charges settle directly to your account, Linq has no + custody of the funds and cannot issue refunds or contest disputes on your + behalf — and there is no refund/dispute endpoint in this API by design. Use + the Stripe Dashboard (or the Stripe API on your own account) for the money + lifecycle after a payment succeeds. + + ## Getting set up + + Open **Agent Pay** in your Linq dashboard + (`https://zero.linqapp.com/organization/payments`), click **Connect Stripe**, + and complete Stripe's onboarding (business details + a bank account). When + your account reaches `charges_enabled`, request creation unlocks; until you + connect Stripe, `POST /v3/payment_requests` returns `403`. You can keep + collecting even while Stripe finishes background verification. + + ## Subscriptions + + Set `mode: subscription` on `POST /v3/payment_requests` to start an + **auto-renewing subscription** instead of a one-time charge. Instead of an + amount, you pass a `price_id` — an active **recurring Price** on your + connected Stripe account (create one in your Stripe Dashboard under + Product catalog; if you sell through Stripe Payment Links today, reuse the + price your link is built from). The recipient pays the first invoice at + the same checkout, and their payment method is saved to the subscription + for automatic renewals. + + The division of labor is deliberate: **Linq handles the first payment, + your Stripe account handles the rest.** The request reaches `succeeded` + when the first invoice is paid; from then on the subscription lives + entirely on your connected account. The response's `stripe` object gives + you the join keys — `customer_id` and `subscription_id` — so renewals, + plan changes, dunning, and cancellation are managed with your own Stripe + Dashboard/API and your own Stripe webhooks. Your `metadata` is stamped on + the Customer and Subscription, so correlating in either direction is + trivial. There are no renewal webhooks from Linq by design. + + ### Free trials + + Add `trial_period_days` (or a fixed `trial_end` timestamp) to start the + subscription with a free trial. The checkout still collects the + recipient's payment method — the pay sheet shows "$0 due today" with the + first charge date — and saves it to the subscription; Stripe bills it + automatically when the trial ends. The request reaches `succeeded` when + the card is collected, and the response carries `trial_end`. If the trial + would end without a payment method on file, the subscription cancels + rather than generating unpayable invoices. Trial lifecycle after checkout + (extending, ending early) is managed in your own Stripe account via + `stripe.subscription_id`. + + A subscription request you cancel (or that expires unpaid) cancels the + incomplete Stripe subscription — nothing lingers on your account. + + ## Pre-created customers + + By default each request stands alone: payment mode attaches no Customer, + and subscription mode creates a fresh one. If you already manage + Customers on your connected account, pass their id as `customer_id` + (`cus_...`) on create — in payment mode the charge lands on that + customer's payment history, and in subscription mode the subscription is + created on them instead of on a new Customer. The id must reference an + existing, non-deleted customer on your connected account or the request + fails with `400`. We never modify a customer you pass — no metadata is + stamped on it. + + ## Sending the link + + Deliver the `checkout_url` as a **`link` message part** via + `POST /v3/chats/{chatId}/messages` — it renders as a rich card with your + branding (title, amount, image) instead of a bare URL, which converts far + better. A `link` part must be the only part in the message. See + [Rich Link Previews](/guides/messaging/sending-messages). + + On a supported iPhone the link opens an **Apple Pay App Clip** — a native, + no-install checkout sheet. Everywhere else (Android, desktop, iPhones + without the App Clip yet) the same URL opens the web checkout, so the link + always works. The App Clip experience for your payment links is registered + automatically by Linq and refreshed whenever you update your Agent Pay + branding; a newly registered experience can take up to ~24 hours to + activate on Apple's side, during which links open the web checkout. + + ## Webhooks + + Subscribe to payment lifecycle events to reconcile server-side rather than + polling: `payment.succeeded`, `payment.canceled`, and `payment.expired`. + Each event carries the payment request id, amount, currency, and your + `metadata`. See [Webhooks](/guides/webhooks). + """ + + @cached_property + def with_raw_response(self) -> PaymentRequestsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/linq-team/linq-python#accessing-raw-response-data-eg-headers + """ + return PaymentRequestsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> PaymentRequestsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/linq-team/linq-python#with_streaming_response + """ + return PaymentRequestsResourceWithStreamingResponse(self) + + def create( + self, + *, + amount: int | Omit = omit, + currency: str | Omit = omit, + customer_id: str | Omit = omit, + description: str | Omit = omit, + metadata: Dict[str, str] | Omit = omit, + mode: Literal["payment", "subscription"] | Omit = omit, + price_id: str | Omit = omit, + quantity: int | Omit = omit, + trial_end: Union[str, datetime] | Omit = omit, + trial_period_days: int | Omit = omit, + idempotency_key: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PaymentRequest: + """ + Creates a payment request and returns a `checkout_url` the recipient opens to + pay with Apple Pay or card. Funds settle directly to your connected Stripe + account. A payment request is independent of any chat; to associate one with a + chat for your records, store the chat id in `metadata`. Requires your connected + account to be `charges_enabled` (returns `403` otherwise). + + Set `mode: subscription` with a recurring `price_id` from your connected Stripe + account to start an **auto-renewing subscription** instead of a one-time charge + — the recipient pays the first invoice at checkout and the response's `stripe` + object carries the customer and subscription ids for the ongoing lifecycle in + your own Stripe account. See the _Subscriptions_ section of the tag overview. + + In either mode, pass `customer_id` to attach the request to an **existing + Customer** on your connected account instead of creating a new one — see + _Pre-created customers_ in the tag overview. + + Args: + amount: Amount to charge, in the currency's minor units (e.g. cents). Must be at least + the payment provider's minimum (50 for `usd`). Required in `payment` mode; must + be omitted in `subscription` mode (the amount comes from the price). + + currency: Three-letter ISO 4217 currency code. Only `usd` is currently supported. Required + in `payment` mode; must be omitted in `subscription` mode (the currency comes + from the price). + + customer_id: Optional id of an **existing Customer** on your connected Stripe account + (`cus_...`) to attach this request to, instead of a new Customer being created. + In `payment` mode the charge lands on that customer's payment history; in + `subscription` mode the subscription is created on them. The customer must exist + (and not be deleted) on your connected account. + + description: Optional description shown to the recipient at checkout. + + metadata: Optional key/value metadata (up to 49 keys) echoed back on retrieval and on + `payment.*` webhooks, and stamped on the Stripe objects we create on your + connected account (the PaymentIntent, and in subscription mode the Subscription + and any Customer created for you — a customer you pass via `customer_id` is + never modified) — use it to correlate a request with your own records (e.g. a + chat id). Keys starting with `linq_` are reserved. + + mode: `payment` (default) collects a one-time charge for `amount` + `currency`. + `subscription` starts an auto-renewing subscription from a recurring `price_id` + on your connected Stripe account: the recipient pays the first invoice at + checkout and Stripe renews it automatically from then on. + + price_id: Subscription mode only (required there): id of an **active recurring Price** on + your connected Stripe account (`price_...`). If you sell through Stripe Payment + Links today, pass the same price the link was built from to get the native + iMessage checkout for it. + + quantity: Subscription mode only — units of the price to subscribe to. + + trial_end: Subscription mode only — end the free trial at a fixed timestamp (must be in the + future) instead of a day count. Mutually exclusive with `trial_period_days`. + + trial_period_days: Subscription mode only — start with a free trial of this many days. The + recipient's card is still collected at checkout (Apple Pay or card), saved to + the subscription, and first charged when the trial ends. Mutually exclusive with + `trial_end`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} + return self._post( + "/v3/payment_requests", + body=maybe_transform( + { + "amount": amount, + "currency": currency, + "customer_id": customer_id, + "description": description, + "metadata": metadata, + "mode": mode, + "price_id": price_id, + "quantity": quantity, + "trial_end": trial_end, + "trial_period_days": trial_period_days, + }, + payment_request_create_params.PaymentRequestCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PaymentRequest, + ) + + def retrieve( + self, + payment_request_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PaymentRequest: + """ + Returns a payment request's status and details. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not payment_request_id: + raise ValueError(f"Expected a non-empty value for `payment_request_id` but received {payment_request_id!r}") + return self._get( + path_template("/v3/payment_requests/{payment_request_id}", payment_request_id=payment_request_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PaymentRequest, + ) + + def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + status: Literal["requested", "succeeded", "canceled", "expired"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PaymentRequestListResponse: + """Lists your payment requests, newest first, for reconciliation. + + Paginate with + `limit` + `offset`; `has_more` indicates whether another page exists. + + Args: + limit: Max results to return (default 20, max 100). + + offset: Number of results to skip. + + status: Filter by lifecycle status. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get( + "/v3/payment_requests", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + "status": status, + }, + payment_request_list_params.PaymentRequestListParams, + ), + ), + cast_to=PaymentRequestListResponse, + ) + + def cancel( + self, + payment_request_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PaymentRequest: + """ + Cancels an unpaid payment request: the underlying payment intent is canceled and + the request moves to `canceled`. A request that is already paid, canceled, or + expired returns 409. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not payment_request_id: + raise ValueError(f"Expected a non-empty value for `payment_request_id` but received {payment_request_id!r}") + return self._post( + path_template("/v3/payment_requests/{payment_request_id}/cancel", payment_request_id=payment_request_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PaymentRequest, + ) + + +class AsyncPaymentRequestsResource(AsyncAPIResource): + """Request a payment from a recipient over iMessage. + + You create a payment + request, send its `checkout_url` to the recipient, and they pay with Apple + Pay or card. Funds settle **directly to your own Stripe account** — Linq + never holds the money. + + ## How it works + + 1. **Create** a payment request with an amount and currency. You get back a + `checkout_url` and a `status` of `requested`. + 2. **Send** the `checkout_url` to the recipient as a `link` message part so + it arrives as a tappable card (see *Sending the link* below). + 3. The recipient **pays** on the hosted checkout (Apple Pay App Clip on a + supported iPhone, web checkout everywhere else). + 4. You receive a **`payment.succeeded`** webhook and the request's `status` + becomes `succeeded`. Requests you don't collect eventually `expire`. + + ## Connected accounts (Stripe Standard, direct charges) + + Agent Pay runs on **Stripe Connect Standard accounts** using **direct + charges**: the charge is created on *your* connected account and **you are + the merchant of record**. That means the money, the payout schedule, the + customer relationship, and the compliance surface are all yours — Linq + orchestrates the request and the checkout but is never in the funds flow. + + **Refunds, disputes, and chargebacks are handled by you, in your own Stripe + Dashboard.** Because charges settle directly to your account, Linq has no + custody of the funds and cannot issue refunds or contest disputes on your + behalf — and there is no refund/dispute endpoint in this API by design. Use + the Stripe Dashboard (or the Stripe API on your own account) for the money + lifecycle after a payment succeeds. + + ## Getting set up + + Open **Agent Pay** in your Linq dashboard + (`https://zero.linqapp.com/organization/payments`), click **Connect Stripe**, + and complete Stripe's onboarding (business details + a bank account). When + your account reaches `charges_enabled`, request creation unlocks; until you + connect Stripe, `POST /v3/payment_requests` returns `403`. You can keep + collecting even while Stripe finishes background verification. + + ## Subscriptions + + Set `mode: subscription` on `POST /v3/payment_requests` to start an + **auto-renewing subscription** instead of a one-time charge. Instead of an + amount, you pass a `price_id` — an active **recurring Price** on your + connected Stripe account (create one in your Stripe Dashboard under + Product catalog; if you sell through Stripe Payment Links today, reuse the + price your link is built from). The recipient pays the first invoice at + the same checkout, and their payment method is saved to the subscription + for automatic renewals. + + The division of labor is deliberate: **Linq handles the first payment, + your Stripe account handles the rest.** The request reaches `succeeded` + when the first invoice is paid; from then on the subscription lives + entirely on your connected account. The response's `stripe` object gives + you the join keys — `customer_id` and `subscription_id` — so renewals, + plan changes, dunning, and cancellation are managed with your own Stripe + Dashboard/API and your own Stripe webhooks. Your `metadata` is stamped on + the Customer and Subscription, so correlating in either direction is + trivial. There are no renewal webhooks from Linq by design. + + ### Free trials + + Add `trial_period_days` (or a fixed `trial_end` timestamp) to start the + subscription with a free trial. The checkout still collects the + recipient's payment method — the pay sheet shows "$0 due today" with the + first charge date — and saves it to the subscription; Stripe bills it + automatically when the trial ends. The request reaches `succeeded` when + the card is collected, and the response carries `trial_end`. If the trial + would end without a payment method on file, the subscription cancels + rather than generating unpayable invoices. Trial lifecycle after checkout + (extending, ending early) is managed in your own Stripe account via + `stripe.subscription_id`. + + A subscription request you cancel (or that expires unpaid) cancels the + incomplete Stripe subscription — nothing lingers on your account. + + ## Pre-created customers + + By default each request stands alone: payment mode attaches no Customer, + and subscription mode creates a fresh one. If you already manage + Customers on your connected account, pass their id as `customer_id` + (`cus_...`) on create — in payment mode the charge lands on that + customer's payment history, and in subscription mode the subscription is + created on them instead of on a new Customer. The id must reference an + existing, non-deleted customer on your connected account or the request + fails with `400`. We never modify a customer you pass — no metadata is + stamped on it. + + ## Sending the link + + Deliver the `checkout_url` as a **`link` message part** via + `POST /v3/chats/{chatId}/messages` — it renders as a rich card with your + branding (title, amount, image) instead of a bare URL, which converts far + better. A `link` part must be the only part in the message. See + [Rich Link Previews](/guides/messaging/sending-messages). + + On a supported iPhone the link opens an **Apple Pay App Clip** — a native, + no-install checkout sheet. Everywhere else (Android, desktop, iPhones + without the App Clip yet) the same URL opens the web checkout, so the link + always works. The App Clip experience for your payment links is registered + automatically by Linq and refreshed whenever you update your Agent Pay + branding; a newly registered experience can take up to ~24 hours to + activate on Apple's side, during which links open the web checkout. + + ## Webhooks + + Subscribe to payment lifecycle events to reconcile server-side rather than + polling: `payment.succeeded`, `payment.canceled`, and `payment.expired`. + Each event carries the payment request id, amount, currency, and your + `metadata`. See [Webhooks](/guides/webhooks). + """ + + @cached_property + def with_raw_response(self) -> AsyncPaymentRequestsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/linq-team/linq-python#accessing-raw-response-data-eg-headers + """ + return AsyncPaymentRequestsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncPaymentRequestsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/linq-team/linq-python#with_streaming_response + """ + return AsyncPaymentRequestsResourceWithStreamingResponse(self) + + async def create( + self, + *, + amount: int | Omit = omit, + currency: str | Omit = omit, + customer_id: str | Omit = omit, + description: str | Omit = omit, + metadata: Dict[str, str] | Omit = omit, + mode: Literal["payment", "subscription"] | Omit = omit, + price_id: str | Omit = omit, + quantity: int | Omit = omit, + trial_end: Union[str, datetime] | Omit = omit, + trial_period_days: int | Omit = omit, + idempotency_key: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PaymentRequest: + """ + Creates a payment request and returns a `checkout_url` the recipient opens to + pay with Apple Pay or card. Funds settle directly to your connected Stripe + account. A payment request is independent of any chat; to associate one with a + chat for your records, store the chat id in `metadata`. Requires your connected + account to be `charges_enabled` (returns `403` otherwise). + + Set `mode: subscription` with a recurring `price_id` from your connected Stripe + account to start an **auto-renewing subscription** instead of a one-time charge + — the recipient pays the first invoice at checkout and the response's `stripe` + object carries the customer and subscription ids for the ongoing lifecycle in + your own Stripe account. See the _Subscriptions_ section of the tag overview. + + In either mode, pass `customer_id` to attach the request to an **existing + Customer** on your connected account instead of creating a new one — see + _Pre-created customers_ in the tag overview. + + Args: + amount: Amount to charge, in the currency's minor units (e.g. cents). Must be at least + the payment provider's minimum (50 for `usd`). Required in `payment` mode; must + be omitted in `subscription` mode (the amount comes from the price). + + currency: Three-letter ISO 4217 currency code. Only `usd` is currently supported. Required + in `payment` mode; must be omitted in `subscription` mode (the currency comes + from the price). + + customer_id: Optional id of an **existing Customer** on your connected Stripe account + (`cus_...`) to attach this request to, instead of a new Customer being created. + In `payment` mode the charge lands on that customer's payment history; in + `subscription` mode the subscription is created on them. The customer must exist + (and not be deleted) on your connected account. + + description: Optional description shown to the recipient at checkout. + + metadata: Optional key/value metadata (up to 49 keys) echoed back on retrieval and on + `payment.*` webhooks, and stamped on the Stripe objects we create on your + connected account (the PaymentIntent, and in subscription mode the Subscription + and any Customer created for you — a customer you pass via `customer_id` is + never modified) — use it to correlate a request with your own records (e.g. a + chat id). Keys starting with `linq_` are reserved. + + mode: `payment` (default) collects a one-time charge for `amount` + `currency`. + `subscription` starts an auto-renewing subscription from a recurring `price_id` + on your connected Stripe account: the recipient pays the first invoice at + checkout and Stripe renews it automatically from then on. + + price_id: Subscription mode only (required there): id of an **active recurring Price** on + your connected Stripe account (`price_...`). If you sell through Stripe Payment + Links today, pass the same price the link was built from to get the native + iMessage checkout for it. + + quantity: Subscription mode only — units of the price to subscribe to. + + trial_end: Subscription mode only — end the free trial at a fixed timestamp (must be in the + future) instead of a day count. Mutually exclusive with `trial_period_days`. + + trial_period_days: Subscription mode only — start with a free trial of this many days. The + recipient's card is still collected at checkout (Apple Pay or card), saved to + the subscription, and first charged when the trial ends. Mutually exclusive with + `trial_end`. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} + return await self._post( + "/v3/payment_requests", + body=await async_maybe_transform( + { + "amount": amount, + "currency": currency, + "customer_id": customer_id, + "description": description, + "metadata": metadata, + "mode": mode, + "price_id": price_id, + "quantity": quantity, + "trial_end": trial_end, + "trial_period_days": trial_period_days, + }, + payment_request_create_params.PaymentRequestCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PaymentRequest, + ) + + async def retrieve( + self, + payment_request_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PaymentRequest: + """ + Returns a payment request's status and details. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not payment_request_id: + raise ValueError(f"Expected a non-empty value for `payment_request_id` but received {payment_request_id!r}") + return await self._get( + path_template("/v3/payment_requests/{payment_request_id}", payment_request_id=payment_request_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PaymentRequest, + ) + + async def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + status: Literal["requested", "succeeded", "canceled", "expired"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PaymentRequestListResponse: + """Lists your payment requests, newest first, for reconciliation. + + Paginate with + `limit` + `offset`; `has_more` indicates whether another page exists. + + Args: + limit: Max results to return (default 20, max 100). + + offset: Number of results to skip. + + status: Filter by lifecycle status. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/v3/payment_requests", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + "status": status, + }, + payment_request_list_params.PaymentRequestListParams, + ), + ), + cast_to=PaymentRequestListResponse, + ) + + async def cancel( + self, + payment_request_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PaymentRequest: + """ + Cancels an unpaid payment request: the underlying payment intent is canceled and + the request moves to `canceled`. A request that is already paid, canceled, or + expired returns 409. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not payment_request_id: + raise ValueError(f"Expected a non-empty value for `payment_request_id` but received {payment_request_id!r}") + return await self._post( + path_template("/v3/payment_requests/{payment_request_id}/cancel", payment_request_id=payment_request_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PaymentRequest, + ) + + +class PaymentRequestsResourceWithRawResponse: + def __init__(self, payment_requests: PaymentRequestsResource) -> None: + self._payment_requests = payment_requests + + self.create = to_raw_response_wrapper( + payment_requests.create, + ) + self.retrieve = to_raw_response_wrapper( + payment_requests.retrieve, + ) + self.list = to_raw_response_wrapper( + payment_requests.list, + ) + self.cancel = to_raw_response_wrapper( + payment_requests.cancel, + ) + + +class AsyncPaymentRequestsResourceWithRawResponse: + def __init__(self, payment_requests: AsyncPaymentRequestsResource) -> None: + self._payment_requests = payment_requests + + self.create = async_to_raw_response_wrapper( + payment_requests.create, + ) + self.retrieve = async_to_raw_response_wrapper( + payment_requests.retrieve, + ) + self.list = async_to_raw_response_wrapper( + payment_requests.list, + ) + self.cancel = async_to_raw_response_wrapper( + payment_requests.cancel, + ) + + +class PaymentRequestsResourceWithStreamingResponse: + def __init__(self, payment_requests: PaymentRequestsResource) -> None: + self._payment_requests = payment_requests + + self.create = to_streamed_response_wrapper( + payment_requests.create, + ) + self.retrieve = to_streamed_response_wrapper( + payment_requests.retrieve, + ) + self.list = to_streamed_response_wrapper( + payment_requests.list, + ) + self.cancel = to_streamed_response_wrapper( + payment_requests.cancel, + ) + + +class AsyncPaymentRequestsResourceWithStreamingResponse: + def __init__(self, payment_requests: AsyncPaymentRequestsResource) -> None: + self._payment_requests = payment_requests + + self.create = async_to_streamed_response_wrapper( + payment_requests.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + payment_requests.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + payment_requests.list, + ) + self.cancel = async_to_streamed_response_wrapper( + payment_requests.cancel, + ) diff --git a/src/linq/types/__init__.py b/src/linq/types/__init__.py index f8e252f..ecfd46b 100644 --- a/src/linq/types/__init__.py +++ b/src/linq/types/__init__.py @@ -18,6 +18,7 @@ from .message_effect import MessageEffect as MessageEffect from .reply_to_param import ReplyToParam as ReplyToParam from .link_part_param import LinkPartParam as LinkPartParam +from .payment_request import PaymentRequest as PaymentRequest from .text_part_param import TextPartParam as TextPartParam from .media_part_param import MediaPartParam as MediaPartParam from .message_event_v2 import MessageEventV2 as MessageEventV2 @@ -54,6 +55,7 @@ from .schemas_text_part_response import SchemasTextPartResponse as SchemasTextPartResponse from .capability_check_RCS_params import CapabilityCheckRCSParams as CapabilityCheckRCSParams from .message_add_reaction_params import MessageAddReactionParams as MessageAddReactionParams +from .payment_request_list_params import PaymentRequestListParams as PaymentRequestListParams from .schemas_media_part_response import SchemasMediaPartResponse as SchemasMediaPartResponse from .webhook_event_list_response import WebhookEventListResponse as WebhookEventListResponse from .attachment_retrieve_response import AttachmentRetrieveResponse as AttachmentRetrieveResponse @@ -64,6 +66,8 @@ from .phone_number_update_response import PhoneNumberUpdateResponse as PhoneNumberUpdateResponse from .reaction_added_webhook_event import ReactionAddedWebhookEvent as ReactionAddedWebhookEvent from .message_add_reaction_response import MessageAddReactionResponse as MessageAddReactionResponse +from .payment_request_create_params import PaymentRequestCreateParams as PaymentRequestCreateParams +from .payment_request_list_response import PaymentRequestListResponse as PaymentRequestListResponse from .contact_card_retrieve_response import ContactCardRetrieveResponse as ContactCardRetrieveResponse from .message_received_webhook_event import MessageReceivedWebhookEvent as MessageReceivedWebhookEvent from .message_update_app_card_params import MessageUpdateAppCardParams as MessageUpdateAppCardParams diff --git a/src/linq/types/message_create_response.py b/src/linq/types/message_create_response.py index f4d8d2d..bbac97e 100644 --- a/src/linq/types/message_create_response.py +++ b/src/linq/types/message_create_response.py @@ -20,8 +20,8 @@ class FromSelection(BaseModel): """ - `reused_active_chat` — reused an existing chat on its healthy line - `new_best_number` — created a new chat on the best available line - - `failover_flagged` — prior chat's line was flagged; created a new chat on a - fresh line + - `failover_flagged` — no existing chat for these recipients was on a line that + could send; created a new chat on a fresh line """ reused_existing_chat: bool diff --git a/src/linq/types/payment_request.py b/src/linq/types/payment_request.py new file mode 100644 index 0000000..0ffdeb4 --- /dev/null +++ b/src/linq/types/payment_request.py @@ -0,0 +1,102 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["PaymentRequest", "Stripe"] + + +class Stripe(BaseModel): + """ + Ids of the Stripe objects created **on your connected account** — + your join keys into your own Stripe Dashboard, webhooks, and API. + After a subscription's first payment succeeds, its ongoing lifecycle + (renewals, plan changes, cancellation) is managed in your Stripe + account using `subscription_id`. + """ + + customer_id: Optional[str] = None + """The Customer this request is attached to (`cus_...`). + + Always set in subscription mode (created for you unless you passed + `customer_id`); set in payment mode only when you passed one. + """ + + payment_intent_id: Optional[str] = None + """The PaymentIntent collected at checkout (`pi_...`).""" + + subscription_id: Optional[str] = None + """Subscription mode — the Subscription (`sub_...`).""" + + +class PaymentRequest(BaseModel): + id: str + """Unique identifier of the payment request.""" + + amount: int + """Amount in the currency's minor units. + + In `subscription` mode this is the recurring amount (price × quantity) the + recipient pays per interval, starting at checkout. + """ + + checkout_url: str + """ + URL the recipient opens to pay: + `https://zero.linqapp.com/pay/{slug}?session=...`, where `{slug}` is your + partner checkout slug. + """ + + created_at: datetime + + currency: str + + mode: Literal["payment", "subscription"] + """Whether this request collects a one-time charge or starts a subscription.""" + + object: str + + status: Literal["requested", "succeeded", "canceled", "expired"] + """Lifecycle status of the payment request.""" + + description: Optional[str] = None + + expires_at: Optional[datetime] = None + """When an unpaid request auto-expires.""" + + interval: Optional[Literal["day", "week", "month", "year"]] = None + """Subscription mode — how often the subscription renews.""" + + interval_count: Optional[int] = None + """Subscription mode — intervals per renewal (e.g. `3` + `month` = quarterly).""" + + metadata: Optional[Dict[str, str]] = None + + paid_at: Optional[datetime] = None + """When the request was paid. Absent until it succeeds.""" + + price_id: Optional[str] = None + """Subscription mode — the recurring price this request subscribes to.""" + + quantity: Optional[int] = None + """Subscription mode — units of the price subscribed to.""" + + stripe: Optional[Stripe] = None + """ + Ids of the Stripe objects created **on your connected account** — your join keys + into your own Stripe Dashboard, webhooks, and API. After a subscription's first + payment succeeds, its ongoing lifecycle (renewals, plan changes, cancellation) + is managed in your Stripe account using `subscription_id`. + """ + + trial_end: Optional[datetime] = None + """Subscription mode — when the free trial ends and the first charge happens. + + Present only on trial requests; `paid_at`/`succeeded` mean the payment method + was collected (no funds move until this time). + """ + + updated_at: Optional[datetime] = None diff --git a/src/linq/types/payment_request_create_params.py b/src/linq/types/payment_request_create_params.py new file mode 100644 index 0000000..b5f4927 --- /dev/null +++ b/src/linq/types/payment_request_create_params.py @@ -0,0 +1,85 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict, Union +from datetime import datetime +from typing_extensions import Literal, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["PaymentRequestCreateParams"] + + +class PaymentRequestCreateParams(TypedDict, total=False): + amount: int + """Amount to charge, in the currency's minor units (e.g. + + cents). Must be at least the payment provider's minimum (50 for `usd`). Required + in `payment` mode; must be omitted in `subscription` mode (the amount comes from + the price). + """ + + currency: str + """Three-letter ISO 4217 currency code. + + Only `usd` is currently supported. Required in `payment` mode; must be omitted + in `subscription` mode (the currency comes from the price). + """ + + customer_id: str + """ + Optional id of an **existing Customer** on your connected Stripe account + (`cus_...`) to attach this request to, instead of a new Customer being created. + In `payment` mode the charge lands on that customer's payment history; in + `subscription` mode the subscription is created on them. The customer must exist + (and not be deleted) on your connected account. + """ + + description: str + """Optional description shown to the recipient at checkout.""" + + metadata: Dict[str, str] + """ + Optional key/value metadata (up to 49 keys) echoed back on retrieval and on + `payment.*` webhooks, and stamped on the Stripe objects we create on your + connected account (the PaymentIntent, and in subscription mode the Subscription + and any Customer created for you — a customer you pass via `customer_id` is + never modified) — use it to correlate a request with your own records (e.g. a + chat id). Keys starting with `linq_` are reserved. + """ + + mode: Literal["payment", "subscription"] + """`payment` (default) collects a one-time charge for `amount` + `currency`. + + `subscription` starts an auto-renewing subscription from a recurring `price_id` + on your connected Stripe account: the recipient pays the first invoice at + checkout and Stripe renews it automatically from then on. + """ + + price_id: str + """ + Subscription mode only (required there): id of an **active recurring Price** on + your connected Stripe account (`price_...`). If you sell through Stripe Payment + Links today, pass the same price the link was built from to get the native + iMessage checkout for it. + """ + + quantity: int + """Subscription mode only — units of the price to subscribe to.""" + + trial_end: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + """ + Subscription mode only — end the free trial at a fixed timestamp (must be in the + future) instead of a day count. Mutually exclusive with `trial_period_days`. + """ + + trial_period_days: int + """ + Subscription mode only — start with a free trial of this many days. The + recipient's card is still collected at checkout (Apple Pay or card), saved to + the subscription, and first charged when the trial ends. Mutually exclusive with + `trial_end`. + """ + + idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] diff --git a/src/linq/types/payment_request_list_params.py b/src/linq/types/payment_request_list_params.py new file mode 100644 index 0000000..dbd7da8 --- /dev/null +++ b/src/linq/types/payment_request_list_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["PaymentRequestListParams"] + + +class PaymentRequestListParams(TypedDict, total=False): + limit: int + """Max results to return (default 20, max 100).""" + + offset: int + """Number of results to skip.""" + + status: Literal["requested", "succeeded", "canceled", "expired"] + """Filter by lifecycle status.""" diff --git a/src/linq/types/payment_request_list_response.py b/src/linq/types/payment_request_list_response.py new file mode 100644 index 0000000..e911b41 --- /dev/null +++ b/src/linq/types/payment_request_list_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import Literal + +from .._models import BaseModel +from .payment_request import PaymentRequest + +__all__ = ["PaymentRequestListResponse"] + + +class PaymentRequestListResponse(BaseModel): + data: List[PaymentRequest] + + has_more: bool + """Whether more results exist beyond this page.""" + + object: Literal["list"] diff --git a/src/linq/types/phone_number_status_updated_webhook_event.py b/src/linq/types/phone_number_status_updated_webhook_event.py index 13ce18c..ce361b7 100644 --- a/src/linq/types/phone_number_status_updated_webhook_event.py +++ b/src/linq/types/phone_number_status_updated_webhook_event.py @@ -97,6 +97,9 @@ class PhoneNumberStatusUpdatedWebhookEvent(BaseModel): "call.no_answer", "location.sharing.started", "location.sharing.stopped", + "payment.succeeded", + "payment.canceled", + "payment.expired", ] """The type of event""" diff --git a/src/linq/types/webhook_event_type.py b/src/linq/types/webhook_event_type.py index f3e760b..7aef1e6 100644 --- a/src/linq/types/webhook_event_type.py +++ b/src/linq/types/webhook_event_type.py @@ -32,4 +32,7 @@ "call.no_answer", "location.sharing.started", "location.sharing.stopped", + "payment.succeeded", + "payment.canceled", + "payment.expired", ] diff --git a/tests/api_resources/test_payment_requests.py b/tests/api_resources/test_payment_requests.py new file mode 100644 index 0000000..be3260c --- /dev/null +++ b/tests/api_resources/test_payment_requests.py @@ -0,0 +1,364 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from linq import LinqAPIV3, AsyncLinqAPIV3 +from linq.types import ( + PaymentRequest, + PaymentRequestListResponse, +) +from linq._utils import parse_datetime +from tests.utils import assert_matches_type + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestPaymentRequests: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: LinqAPIV3) -> None: + payment_request = client.payment_requests.create() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: LinqAPIV3) -> None: + payment_request = client.payment_requests.create( + amount=497, + currency="usd", + customer_id="cus_QAbCdEfGhIjKlMn", + description="Coffee with Ava", + metadata={"order_id": "order_8675309"}, + mode="payment", + price_id="price_1QAbCdEfGhIjKlMn", + quantity=1, + trial_end=parse_datetime("2019-12-27T18:11:19.117Z"), + trial_period_days=14, + idempotency_key="pr-abc123xyz", + ) + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: LinqAPIV3) -> None: + response = client.payment_requests.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + payment_request = response.parse() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: LinqAPIV3) -> None: + with client.payment_requests.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + payment_request = response.parse() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: LinqAPIV3) -> None: + payment_request = client.payment_requests.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: LinqAPIV3) -> None: + response = client.payment_requests.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + payment_request = response.parse() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: LinqAPIV3) -> None: + with client.payment_requests.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + payment_request = response.parse() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: LinqAPIV3) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `payment_request_id` but received ''"): + client.payment_requests.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: LinqAPIV3) -> None: + payment_request = client.payment_requests.list() + assert_matches_type(PaymentRequestListResponse, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: LinqAPIV3) -> None: + payment_request = client.payment_requests.list( + limit=1, + offset=0, + status="requested", + ) + assert_matches_type(PaymentRequestListResponse, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: LinqAPIV3) -> None: + response = client.payment_requests.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + payment_request = response.parse() + assert_matches_type(PaymentRequestListResponse, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: LinqAPIV3) -> None: + with client.payment_requests.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + payment_request = response.parse() + assert_matches_type(PaymentRequestListResponse, payment_request, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_cancel(self, client: LinqAPIV3) -> None: + payment_request = client.payment_requests.cancel( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_cancel(self, client: LinqAPIV3) -> None: + response = client.payment_requests.with_raw_response.cancel( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + payment_request = response.parse() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_cancel(self, client: LinqAPIV3) -> None: + with client.payment_requests.with_streaming_response.cancel( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + payment_request = response.parse() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_cancel(self, client: LinqAPIV3) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `payment_request_id` but received ''"): + client.payment_requests.with_raw_response.cancel( + "", + ) + + +class TestAsyncPaymentRequests: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncLinqAPIV3) -> None: + payment_request = await async_client.payment_requests.create() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncLinqAPIV3) -> None: + payment_request = await async_client.payment_requests.create( + amount=497, + currency="usd", + customer_id="cus_QAbCdEfGhIjKlMn", + description="Coffee with Ava", + metadata={"order_id": "order_8675309"}, + mode="payment", + price_id="price_1QAbCdEfGhIjKlMn", + quantity=1, + trial_end=parse_datetime("2019-12-27T18:11:19.117Z"), + trial_period_days=14, + idempotency_key="pr-abc123xyz", + ) + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncLinqAPIV3) -> None: + response = await async_client.payment_requests.with_raw_response.create() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + payment_request = await response.parse() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncLinqAPIV3) -> None: + async with async_client.payment_requests.with_streaming_response.create() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + payment_request = await response.parse() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncLinqAPIV3) -> None: + payment_request = await async_client.payment_requests.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncLinqAPIV3) -> None: + response = await async_client.payment_requests.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + payment_request = await response.parse() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncLinqAPIV3) -> None: + async with async_client.payment_requests.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + payment_request = await response.parse() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncLinqAPIV3) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `payment_request_id` but received ''"): + await async_client.payment_requests.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncLinqAPIV3) -> None: + payment_request = await async_client.payment_requests.list() + assert_matches_type(PaymentRequestListResponse, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncLinqAPIV3) -> None: + payment_request = await async_client.payment_requests.list( + limit=1, + offset=0, + status="requested", + ) + assert_matches_type(PaymentRequestListResponse, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncLinqAPIV3) -> None: + response = await async_client.payment_requests.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + payment_request = await response.parse() + assert_matches_type(PaymentRequestListResponse, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncLinqAPIV3) -> None: + async with async_client.payment_requests.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + payment_request = await response.parse() + assert_matches_type(PaymentRequestListResponse, payment_request, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_cancel(self, async_client: AsyncLinqAPIV3) -> None: + payment_request = await async_client.payment_requests.cancel( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_cancel(self, async_client: AsyncLinqAPIV3) -> None: + response = await async_client.payment_requests.with_raw_response.cancel( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + payment_request = await response.parse() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_cancel(self, async_client: AsyncLinqAPIV3) -> None: + async with async_client.payment_requests.with_streaming_response.cancel( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + payment_request = await response.parse() + assert_matches_type(PaymentRequest, payment_request, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_cancel(self, async_client: AsyncLinqAPIV3) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `payment_request_id` but received ''"): + await async_client.payment_requests.with_raw_response.cancel( + "", + ) diff --git a/uv.lock b/uv.lock index ee08702..f3050eb 100644 --- a/uv.lock +++ b/uv.lock @@ -530,7 +530,7 @@ wheels = [ [[package]] name = "linq-python" -version = "0.15.0" +version = "0.15.1" source = { editable = "." } dependencies = [ { name = "anyio" },