Collect Payments with Tap¶
Connect Tap Payments to Invora Billing and collect money from your customers through a Tap-hosted checkout page. This guide covers connecting your Tap account, generating a payment link for an invoice, bundling invoices into a payment request, charging saved cards, and reacting to payment outcomes through webhooks.
Tap is the default gateway for the MENA region (cards, Apple Pay, mada, KNET, benefit). For the full list of supported gateways see Billing & subscriptions.
All examples use the REST/JSON surface (gRPC-JSON transcoding): JSON bodies, camelCase field names, and a bearer token from the Authentication guide. Base URLs:
| Environment | Base URL |
|---|---|
| Production | https://gateway.invora.app |
| Staging | https://stg-gateway.invora.app |
Set a token once and reuse it in every example below:
How Payment Collection Works¶
- Connect Tap once — register your Tap secret key as a payment provider on your tenant.
- Link each billing customer to Tap — call
CustomersService.UpdatewithpaymentProvider: "PROVIDER_TYPE_TAP"andsyncWithProvider: true. This creates the provider-customer record thatGetPaymentUrlrequires. This step is mandatory — skipping it returns400 no_linked_payment_provider. - Generate a payment link for a finalized invoice, or send a payment request that bundles one or more invoices.
- The customer pays on the Tap-hosted checkout page (card entry + 3D Secure handled by Tap).
- Invora reconciles the result and emits billing webhook events (
payment.succeeded,invoice.payment_status_updated, …) to your registered endpoint.
flowchart LR
A[Connect Tap<br/>once per tenant] --> B[Link customer to Tap<br/>once per customer]
B --> C[Finalize invoice]
C --> D{Collect}
D -->|single invoice| E[Get payment URL]
D -->|one or more invoices| F[Create payment request]
E --> G[Customer pays<br/>on Tap checkout]
F --> G
G --> H[Invora reconciles]
H --> I[Webhook events<br/>to your endpoint]
Prerequisites¶
- Billing is provisioned for your tenant. This happens automatically at onboarding. If billing calls return
FAILED_PRECONDITION, contact support@invora.app. - You have a billing token. A tenant owner automatically holds the
Invora.Billingrole — no separate access request is needed. Obtain a token with the client-credentials or password grant. - You have a Tap secret key from the Tap dashboard:
- Sandbox (test):
sk_test_… - Production (live):
sk_live_…
- Sandbox (test):
Never send a live key to a non-production environment
Use sk_test_… keys against stg-gateway.invora.app. A live sk_live_… key on staging will attempt real charges.
The try-it console pre-fills Tap's shared test key
The interactive Send buttons in these docs default the Tap key field to Tap's own published sandbox key (from its test-keys page) — it is shared across everyone reading Tap's docs, so it's fine for a quick try but prefer your own test key from the Tap dashboard for anything real.
1. Connect Tap¶
Register your Tap credentials. This creates a provider record used for every later payment
operation. Save the returned id.
curl -X POST https://gateway.invora.app/api/billing/v2/integrations/tap \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"apiKey": "sk_test_YOUR_TAP_KEY",
"code": "tap_main",
"name": "Tap Payments",
"successRedirectUrl": "https://your-app.com/payment/success",
"supports3ds": true,
"saveCardEnabled": false
}'
{
"tapProvider": {
"id": "7705b5eb-30eb-4ca4-ba90-4478c04ff5b4",
"code": "tap_main",
"name": "Tap Payments",
"successRedirectUrl": "https://your-app.com/payment/success"
}
}
| Field | Required | Description |
|---|---|---|
apiKey |
✓ | Your Tap secret key. Write-only — it is never returned in responses. |
code |
✓ | Unique slug for this provider within your tenant (e.g. tap_main). |
name |
✓ | Display name. |
successRedirectUrl |
Where the customer's browser returns after the Tap checkout completes or is abandoned. | |
supports3ds |
Enable 3D Secure on customer-initiated charges. Defaults to true. |
|
saveCardEnabled |
Store the card for future charges. Requires Tap KYC approval — see Saved cards. |
Update the provider¶
Change the redirect URL or toggles later. The provider id is a path parameter; send only
the fields you want to change.
curl -X PUT https://gateway.invora.app/api/billing/v2/integrations/tap/7705b5eb-30eb-4ca4-ba90-4478c04ff5b4 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "successRedirectUrl": "https://your-app.com/payment/thank-you" }'
The API key cannot be changed
The secret key is write-once. To rotate it, delete the provider and create a new one.
2. Link the Customer to Tap¶
Before you can generate a payment link, you must link the billing customer to the Tap provider.
This creates the provider-customer record (PaymentProviderCustomers::TapCustomer) that
GetPaymentUrl checks at runtime. Do this once per customer, any time after step 1.
Use the customer's internal billing id (returned by CustomersService.Create or List) and
the same code you chose when connecting Tap.
curl -X PUT https://gateway.invora.app/api/billing/v2/customers/BILLING_CUSTOMER_ID \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"paymentProvider": "PROVIDER_TYPE_TAP",
"paymentProviderCode": "tap_main",
"providerCustomer": { "syncWithProvider": true },
"updateMask": "paymentProvider,paymentProviderCode,providerCustomer"
}'
{
"customer": {
"id": "BILLING_CUSTOMER_ID",
"paymentProvider": "PROVIDER_TYPE_TAP",
"paymentProviderCode": "tap_main"
}
}
| Field | Required | Description |
|---|---|---|
paymentProvider |
✓ | Provider type enum. Use "PROVIDER_TYPE_TAP" for Tap Payments (the full enum name, not the slug tap). |
paymentProviderCode |
✓ | The code from step 1 (e.g. tap_main). |
providerCustomer.syncWithProvider |
✓ | Set true to create the provider-customer record in the billing backend. Without this the GetPaymentUrl call returns 400 no_linked_payment_provider. |
updateMask |
✓ | Comma-separated list of camelCase field names to update (gRPC-JSON FieldMask convention). Must include all three fields above. |
400 no_linked_payment_provider
If you call GetPaymentUrl before completing this step, the API returns:
3. Generate a Payment Link for an Invoice¶
Given a finalized invoice, get a Tap-hosted checkout URL and redirect the customer to it.
curl -X POST https://gateway.invora.app/api/billing/v2/payments/get-payment-url \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "invoiceId": "01963e21-0e46-7000-8d3a-c7f9b2e15a4c" }'
Redirect the customer's browser to paymentUrl. After they complete or abandon the payment, Tap
returns them to the successRedirectUrl from step 1. The final result arrives asynchronously via
webhooks — do not rely on the redirect alone to confirm payment.
The invoice's billing customer must be linked to a Tap provider (step 2) and the invoice must be payable (non-zero, awaiting payment). If you skipped step 2, you will see the error below — return to step 2 first.
Common errors:
{
"code": 3,
"message": "Validation errors: {\"base\":[\"no_linked_payment_provider\"]}",
"details": []
}
4. Send a Payment Request¶
A payment request bundles one or more outstanding invoices into a single payable unit, optionally emails the customer, and — when a saved card is available — can charge it automatically.
curl -X POST https://gateway.invora.app/api/billing/v2/payments/requests \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"externalCustomerId": "cust_001",
"billingProviderInvoiceIds": ["01963e21-0e46-7000-8d3a-c7f9b2e15a4c", "01963f20-1a4c-7000-9e2b-d8a0c3f26b5d"],
"email": "customer@example.com"
}'
{
"paymentRequest": {
"id": "01963f30-2b5d-7000-ae3c-e9b1d4073c6e",
"amountCents": "15000",
"amountCurrency": "CURRENCY_ENUM_SAR",
"email": "customer@example.com",
"paymentStatus": "INVOICE_PAYMENT_STATUS_TYPE_PENDING",
"createdAt": "2026-06-28T10:00:00Z"
}
}
| Field | Required | Description |
|---|---|---|
externalCustomerId |
✓ | Your customer's external ID in Invora Billing. |
billingProviderInvoiceIds |
✓ | Invoice IDs to include in the request. |
email |
Address to send the payment-request email. | |
paymentMethod |
A saved payment method to auto-charge without redirecting the customer — see Saved cards. |
Amounts are stringified integers
amountCents is a 64-bit integer serialized as a JSON string (gRPC-JSON convention), and
amountCurrency uses the full enum name (CURRENCY_ENUM_SAR). 15000 means 150.00 SAR.
Use the returned id to manage the request:
# List payment requests
curl -X POST https://gateway.invora.app/api/billing/v2/payments/requests/list \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}'
# Download a PDF receipt after payment
curl -X GET https://gateway.invora.app/api/billing/v2/payments/requests/01963f30-2b5d-7000-ae3c-e9b1d4073c6e/receipt \
-H "Authorization: Bearer $TOKEN" -o receipt.pdf
# Resend the request email
curl -X POST https://gateway.invora.app/api/billing/v2/payments/requests/01963f30-2b5d-7000-ae3c-e9b1d4073c6e/resend-email \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}'
If externalCustomerId does not exist, the API returns 404 with "message": "customer_not_found".
Saved Cards and Recurring Payments¶
With saveCardEnabled: true, the customer's first checkout stores their card against their
customer record. For subsequent invoices, Invora charges that card automatically — no redirect, no
customer interaction. This is a merchant-initiated transaction.
To use it, pass the saved paymentMethod reference in a payment request
instead of redirecting the customer.
Requirements:
saveCardEnabled: trueon the Tap provider (set at create or update time).- Your Tap account is approved for merchant-initiated / recurring payments (Tap KYC).
Saved-card activation requires Tap KYC
Setting saveCardEnabled: true succeeds even if your Tap account is not yet approved for
recurring charges. The failure surfaces later, on the first attempt to reuse a stored card.
Test the full saved-card flow in Tap's sandbox before going live.
Webhook Events¶
Subscribe to billing events to react to payment outcomes in real time. See Webhooks for endpoint registration, signature verification, and the retry policy.
Two distinct webhook channels
Tap delivers raw charge results to Invora's internal receiver — you never configure or see that. What you receive are Invora billing events, delivered from Invora to the webhook endpoint you register. Those are the events below.
Subscribe to these event types (use the name without the EVENT_TYPE_ prefix):
| Event | Delivered webhook_type |
Fires when |
|---|---|---|
PAYMENT_SUCCEEDED |
payment.succeeded |
A payment is captured. |
INVOICE_PAYMENT_STATUS_UPDATED |
invoice.payment_status_updated |
An invoice's payment status becomes succeeded or failed. |
INVOICE_PAYMENT_FAILURE |
invoice.payment_failure |
An invoice payment attempt fails. |
PAYMENT_REQUIRES_ACTION |
payment.requires_action |
3D Secure or other customer action is required. |
PAYMENT_REQUEST_PAYMENT_STATUS_UPDATED |
payment_request.payment_status_updated |
A payment request's status changes. |
PAYMENT_REQUEST_PAYMENT_FAILURE |
payment_request.payment_failure |
A payment-request attempt fails. |
PAYMENT_RECEIPT_CREATED |
payment_receipt.created |
A receipt is generated after collection. |
CUSTOMER_PAYMENT_PROVIDER_CREATED |
customer.payment_provider_created |
A customer is linked to a payment provider. |
A delivered event looks like this:
{
"webhook_type": "payment.succeeded",
"object_type": "payment",
"organization_id": "317842111002338820",
"payment": {
"invora_id": "01963f40-3c6e-7000-be4d-fac2e518fd7f",
"external_customer_id": "cust_001",
"invoice_ids": ["01963e21-0e46-7000-8d3a-c7f9b2e15a4c"],
"amount_cents": 15000,
"amount_currency": "SAR",
"status": "CAPTURED",
"payment_status": "succeeded",
"type": "tap",
"provider_payment_id": "chg_xxxxxxxxxxxxxxxxxxxxxxxx",
"payment_provider_code": "tap_main",
"created_at": "2026-06-28T10:05:00Z"
}
}
Webhook payloads use snake_case
Event payloads delivered to your endpoint use snake_case keys. This differs from the REST API responses above, which use camelCase.
Payment status mapping¶
Invora maps each Tap charge status to a billing payment_status:
| Tap status | Invora payment_status |
Meaning |
|---|---|---|
CAPTURED |
succeeded |
Payment captured; funds will settle. |
INITIATED |
(in progress) | Awaiting customer action on the checkout page. |
DECLINED |
failed |
Card declined by the issuer. |
RESTRICTED |
failed |
Blocked by Tap risk rules. |
FAILED |
failed |
Generic failure. |
TIMEDOUT |
failed |
Customer did not complete checkout in time. |
ABANDONED |
failed |
Customer left the checkout page. |
CANCELLED |
failed |
Charge cancelled. |
EXPIRED |
failed |
Checkout link expired. |
UNKNOWN |
failed |
Unrecognized status — treat as failed. |
Error Reference¶
Every error response includes a code, a message, and a details array. See
Error handling for the full status-code reference and retry strategy.
| HTTP | gRPC code | Cause |
|---|---|---|
| 400 | INVALID_ARGUMENT (3) |
Missing required field, or no payment provider linked to the customer (no_linked_payment_provider). |
| 400 | FAILED_PRECONDITION (9) |
Billing is not provisioned for this tenant. See Prerequisites. |
| 404 | NOT_FOUND (5) |
Invoice or customer does not exist (customer_not_found). |
| 429 | RESOURCE_EXHAUSTED (8) |
Rate limit exceeded. Back off and retry. |
| 503 | UNAVAILABLE (14) |
Tap is temporarily unreachable. Retry with exponential backoff. |
End-to-End Example¶
Collect payment on a single finalized invoice, start to finish.
# 1. Connect Tap (once per tenant)
curl -X POST https://gateway.invora.app/api/billing/v2/integrations/tap \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"apiKey": "sk_test_YOUR_TAP_KEY",
"code": "tap_main",
"name": "Tap Payments",
"successRedirectUrl": "https://your-app.com/payment/success",
"supports3ds": true
}'
# -> { "tapProvider": { "id": "7705b5eb-...", "code": "tap_main", ... } }
# 2. Link the billing customer to Tap (once per customer)
# Use the customer's internal billing id from CustomersService.Create / List.
curl -X PUT https://gateway.invora.app/api/billing/v2/customers/BILLING_CUSTOMER_ID \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"paymentProvider": "PROVIDER_TYPE_TAP",
"paymentProviderCode": "tap_main",
"providerCustomer": { "syncWithProvider": true },
"updateMask": "paymentProvider,paymentProviderCode,providerCustomer"
}'
# -> { "customer": { "id": "BILLING_CUSTOMER_ID", "paymentProvider": "PROVIDER_TYPE_TAP", ... } }
# 3. Get a payment link for invoice 01963e21-0e46-7000-8d3a-c7f9b2e15a4c
curl -X POST https://gateway.invora.app/api/billing/v2/payments/get-payment-url \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "invoiceId": "01963e21-0e46-7000-8d3a-c7f9b2e15a4c" }'
# -> { "paymentUrl": "https://secure.tap.company/v2/..." }
# 4. Redirect the customer to paymentUrl. Tap handles card entry + 3DS,
# then returns them to successRedirectUrl.
# 5. Your webhook endpoint receives payment.succeeded, then
# invoice.payment_status_updated (payment_status: "succeeded").
# 6. Download the receipt
curl -X GET https://gateway.invora.app/api/billing/v2/payments/requests/<request_id>/receipt \
-H "Authorization: Bearer $TOKEN" -o receipt.pdf
Related¶
- Authentication — obtaining access tokens.
- Billing & subscriptions — plans, subscriptions, customers, and payment providers.
- Webhooks — endpoint registration, signatures, and retry policy.
- Error handling — status codes and retry strategy.
- Marketplace & intermediary invoicing — Tap payment collection in multi-tenant platform scenarios.