Polling

Polling is a way to retrieve the list of payments that both senders and receivers need to process.

Overview flow chart

A typical workflow for polling for payments that are ready for processing, is as follows:

Polling flowchart

Step-by-step implementation

  1. Payment states
  2. Create the request
  3. Read the response
  4. Send payments for processing
  5. Create the pollers independently from each other

1. Payment states

You can use the same technique to poll for payments in any state or substate.

Senders poll for payments in the following states:

  • EXECUTED (with sub-states)
  • FAILED
  • COMPLETED

Receivers poll for payments in the following states:

  • ACCEPTED
  • EXECUTED
  • FAILED

For more information on Ripple Payments states, see Payment states .

A substate is a label that provides communication between the SENDING and RECEIVING RippleNet instances. A substate label is only present when the payment is in the EXECUTED state.

Note:

For more information on Ripple Payments states, see Payment states .

In the following section, we'll focus on building the poller for payments in the ACCEPTED state.

2. Create the request

Use the GET /v4/payments operation to poll the payments by states.

Request example:

Copy
Copied!
GET /v4/payments?page=0&size=100&states=ACCEPTED&sort_field=CREATED_AT&sort_direction=DESC
Authorization: Bearer eyJhbGci...zI1NiOlsi

The GET /v4/payments operation accepts many parameters. Find all the parameters and their descriptions in the API reference documentation.

It's important to use them as they allow you to filter the payments you poll.

In the request example considered here, these are parameter values we'll use:

  • page=0
  • size=100
  • states=ACCEPTED
  • sort_field=CREATED_AT
  • sort_direction=DESC

page

The page parameter controls the pagination process, which we explain in 3. Read the response below.

The default page value is 0.

size

The size parameter indicates the number of payments on one page.

The default size value is 10 and its maximum is 100.

As a best practice, set the size value to 100 in your middleware.

states

The states parameter lets you specify a list of payment states that you want to poll.

In our example, to poll for ACCEPTED payments, we set states=ACCEPTED.

Best practice:

Set states to one state only, as each poller needs to be independent from other pollers. See also 5. Create the pollers independently from each other.

Note: The state parameter is deprecated, use states.

sort_field and sort_direction

The sort_field and sort_direction parameters control the sorting of the returned list of payments.

The default value for sort_field is MODIFIED_AT.

The default value for sort_direction is DESC.

Best practice:

Use these two fields with the following values sort_field=CREATED_AT and sort_direction=DESC. This causes the oldest payments to appear first in the response list as you need to process the oldest payments before they expire.

3. Read the response

Before you send the payments for processing (which is a different action than polling), read and analyze the response.

Here is a sample response from GET /v4/payments:

Click to view the full JSON response ...
Copy
Copied!
{
    "first": true,
    "last": true,
    "number": "0",
    "numberOfElements": "1",
    "size": "100",
    "totalElements": "1",
    "totalPages": "1",
    "sort": [
        {
            "direction": "DESC",
            "property": "CREATED_AT",
            "ignoreCase": false,
            "nullHandling": "NATIVE",
            "ascending": false,
            "descending": true
        }
    ],
    "content": [
        {
            "payment_id": "f293947c-9067-47d7-a69e-6658070ed482",
            "contract_hash": "c6db748bf84b7ed38b0fdc3bd6bdcca8d072af27b473ad0df47aa450b6c70396",
            "payment_state": "ACCEPTED",
            "modified_at": "2021-03-03T04:40:58.629Z",
            "contract": {
                "sender_end_to_end_id": "test",
                "created_at": "2021-03-03T04:40:57.812Z",
                "expires_at": "2021-03-26T08:14:14.839Z",
                "quote": {
                    "quote_id": "642fd286-d3a7-4a75-a14b-86ca9a504922",
                    "created_at": "2021-03-03T04:40:54.839Z",
                    "expires_at": "2021-03-26T08:14:14.839Z",
                    "type": "SENDER_AMOUNT",
                    "price_guarantee": "FIRM",
                    "sender_address": "trans_usd_sender@test.cloud.blueprint1",
                    "receiver_address": "trans_gbp_receiver@test.cloud.blueprint2",
                    "amount": "10.000000000",
                    "currency_code": "USD",
                    "currency_code_filter": null,
                    "service_type": null,
                    "quote_elements": [
                        {
                            "quote_element_id": "23b21542-4b54-4c3c-a2dd-f551fe026194",
                            "quote_element_type": "TRANSFER",
                            "quote_element_order": "1",
                            "sender_address": "trans_usd_sender@test.cloud.blueprint1",
                            "receiver_address": "conct_usd_sender2@test.cloud.blueprint1",
                            "sending_amount": "10.000000000",
                            "receiving_amount": "10.000000000",
                            "sending_fee": "0.000000000",
                            "receiving_fee": "0.000000000",
                            "sending_currency_code": null,
                            "receiving_currency_code": null,
                            "fx_rate": null,
                            "transfer_currency_code": "USD"
                        },
                        {
                            "quote_element_id": "3e31fa45-731a-4a35-8cd9-0100b19f7b66",
                            "quote_element_type": "TRANSFER",
                            "quote_element_order": "2",
                            "sender_address": "conct_usd_sender@test.cloud.blueprint2",
                            "receiver_address": "alias_usd_receiver@test.cloud.blueprint2",
                            "sending_amount": "10.000000000",
                            "receiving_amount": "10.000000000",
                            "sending_fee": "0.000000000",
                            "receiving_fee": "0.000000000",
                            "sending_currency_code": null,
                            "receiving_currency_code": null,
                            "fx_rate": null,
                            "transfer_currency_code": "USD"
                        },
                        {
                            "quote_element_id": "66b80732-66a7-4a77-ab2d-3853d6849e98",
                            "quote_element_type": "EXCHANGE",
                            "quote_element_order": "3",
                            "sender_address": "alias_usd_receiver@test.cloud.blueprint2",
                            "receiver_address": "conct_gbp_receiver@test.cloud.blueprint2",
                            "sending_amount": "10.000000000",
                            "receiving_amount": "12.000000000",
                            "sending_fee": "0.000000000",
                            "receiving_fee": "0.000000000",
                            "sending_currency_code": "USD",
                            "receiving_currency_code": "GBP",
                            "fx_rate": {
                                "rate": "1.200000000",
                                "base_currency_code": "USD",
                                "counter_currency_code": "GBP",
                                "type": "buy"
                            },
                            "transfer_currency_code": null
                        },
                        {
                            "quote_element_id": "051885bf-a4be-4661-800e-40e05ac1f349",
                            "quote_element_type": "TRANSFER",
                            "quote_element_order": "4",
                            "sender_address": "conct_gbp_receiver@test.cloud.blueprint2",
                            "receiver_address": "trans_gbp_receiver@test.cloud.blueprint2",
                            "sending_amount": "12.000000000",
                            "receiving_amount": "12.000000000",
                            "sending_fee": "0.000000000",
                            "receiving_fee": "0.000000000",
                            "sending_currency_code": null,
                            "receiving_currency_code": null,
                            "fx_rate": null,
                            "transfer_currency_code": "GBP"
                        }
                    ],
                    "liquidity_warning": null,
                    "payment_method": null,
                    "payment_method_fields": null,
                    "payout_method_info": null
                },
                "fee_info": null
            },
            "ripplenet_info": [],
            "execution_condition": "PrefixSha256Condition{subtypes=[ED25519-SHA-256], type=PREFIX-SHA-256, fingerprint=VK9hSv6zUK9czMZCiZ9n_BWZrDGEbRQumi641B76VFQ, cost=132360}",
            "crypto_transaction_id": null,
            "validator": "test.cloud.blueprint1",
            "payment_type": "REGULAR",
            "returns_payment_with_id": null,
            "returned_by_payment_with_id": null,
            "execution_results": [],
            "internal_info": {
                "connector_role": "RECEIVING",
                "labels": [],
                "internal_id": null
            },
            "user_info": [
                {
                    "node_address": "test.cloud.blueprint1",
                    "accepted": [
                        {
                            "json": {
                                "end_to_end_id": "20201018041040",
                                "DbtrAcct": {},
                                "DbtrAgt": {},
                                "DbtrAgtAcct": {},
                                "InstForCdtrAgt": {},
                                "Purp": {},
                                "RgltryRptg": {},
                                "RltdRmtInf": {},
                                "UltmtCdtr": {},
                                "Cdtr": {
                                    "Nm": "Peppa Potts",
                                    "PstlAdr": {
                                        "AdrLine": "Somewhere in Malaysia",
                                        "Ctry": "MY",
                                        "PstCd": "12345"
                                    },
                                    "Id": {
                                        "PrvtId": {
                                            "Othr": {
                                                "Id": "AA8423234",
                                                "SchmeNm": {
                                                    "Cd": "ID"
                                                }
                                            }
                                        }
                                    }
                                },
                                "CdtrAcct": {
                                    "Id": {
                                        "Othr": {
                                            "Id": "0123456789"
                                        }
                                    },
                                    "Tp": {
                                        "Cd": "Saving"
                                    }
                                },
                                "CdtrAgt": {
                                    "FinInstnId": {
                                        "BICFI": "ABCDEF",
                                        "Nm": "Bank A"
                                    }
                                },
                                "Dbtr": {
                                    "Nm": "Robert Downey",
                                    "PstlAdr": {
                                        "AdrLine": "Tampines St 81",
                                        "Ctry": "SG",
                                        "PstCd": "543210"
                                    },
                                    "Id": {
                                        "PrvtId": {
                                            "Othr": {
                                                "Id": "AA8463912",
                                                "SchmeNm": {
                                                    "Cd": "ID"
                                                }
                                            }
                                        }
                                    }
                                },
                                "RmtInf": {
                                    "Ustrd": "20201018041010"
                                },
                                "PmtTpInf": {
                                    "CtgyPurp": {
                                        "Prtry": "IR02836"
                                    }
                                }
                            },
                            "created_at": "2021-03-03T04:40:58.449Z",
                            "subState": ""
                        }
                    ],
                    "locked": [],
                    "lock_declined": [],
                    "retry_accept": [],
                    "retry_settlement": [],
                    "settlement": [],
                    "settlement_declined": [],
                    "failed": [],
                    "executed": [],
                    "completed": [],
                    "forwarded": [],
                    "returned": []
                }
            ]
        }
    ]
}

The first fields are important as they tell you if you need to use pagination to read the full results, as described next.

Pagination

In the example below, the values differ from the example above to illustrate the concept of pagination:

Copy
Copied!
{
    "first": true,
    "last": false,
    "number": "0",
    "numberOfElements": "100",
    "size": "100",
    "totalElements": "300",
    "totalPages": "3",
    ...
}

This tells us the RippleNet API is returning three pages (numbered from 0 to 2) with 100 payments per page, so we need to use pagination to retrieve all the payments.

If totalElements is higher than size, then totalPages will be higher than 1.

The last field, if true, indicates that the page is the last one. This is the field you can use to programmatically handle the pagination.

Implement logic in your middleware to keep calling GET /v4/payments, incrementing page by 1 for each call, until last=true.

Pagination flowchart

Click to view example Java implementation of pagination ...
Copy
Copied!
Payments payments = paymentsServices.getPaymentsFromAllPages(getPaymentsParams);

...

@Override
public Payments getPaymentsFromAllPages(GetPaymentsParams getPaymentsParams)
        throws RippleNetProblemException, RnAuthException {

    int page = 0;
    getPaymentsParams.setPage(page);

    // We retrieve the payments on page 0 first
    Payments payments = getPayments(getPaymentsParams);
    int numberOfElements = payments.getNumberOfElements();

    // If the page is not the last one
    while (!payments.isLast()) {
        // We increase the page by 1
        page++;
        getPaymentsParams.setPage(page);

        // We retrieve the payments on the incremented page
        Payments paymentsTemporary = getPayments(getPaymentsParams);

        // And for each payment on the new page
        for (Payment payment : paymentsTemporary.getContent()) {
            // we add it to the global list of Payments
            payments.addContentItem(payment);
            // we also update the total number of payments/elements in the payments object
            numberOfElements++;
            payments.setNumberOfElements(numberOfElements);
        }
        // Finally, at the end of the while loop, we update the first/last booleans
        // If page 1, for example, is the last, then the while loop will exit
        payments.setLast(paymentsTemporary.isLast());
        payments.setFirst(paymentsTemporary.isFirst());
    }

    LOGGER.info("Total amount of payments retrieved: {}", numberOfElements);
    LOGGER.info("Last page where payments were retrieved is page {}",
            getPaymentsParams.getPage());

    // Return the list of Payments
    return payments;
}

4. Send payments for processing

After retrieving all the payments with your poller, send the payments for processing.

For example, for a payment in ACCEPTED state:

  • Send the payment for compliance review.
  • Validate that its user_info has been received.
  • Lock, reject, or fail the payment.
Click to view example Java implementation of payment processing ...
Copy
Copied!
if (payments != null) {
    if (payments.getNumberOfElements() > 0) {
        paymentsProcessor.processPayments(payments, hasSubStates);
    } else {
        LOGGER.info("No payments in {} states retrieved", getPaymentsParams.getStates());
    }
}

5. Create the pollers independently from each other

Best practice:

To make it easier to control the execution of the pollers independently, a suggested poller-based workflow starts with a separate poller for each state, where each poller is in a separate file. To avoid confusion (and for clarity), name your pollers based on the state the payment is in when the poller processes the payment. For example, for the poller for EXECUTED payments, name it ExecutedPaymentsPoller.

The following actions need to be possible on one poller without affecting the others:

  • Enable or disable a poller.
  • Define an initial delay after startup.
  • Define a fixed delay between two executions of the same poller.

Create your pollers as follows:

  1. Create a separate poller for each state by duplicating the ACCEPTED poller, and adapting the parameters ( states , etc.) for each poller.

    Place each poller in its own file. For example:

    • AcceptedPaymentsPoller
    • ExecutedPaymentsPoller
    • CompletedPaymentsPoller
    • FailedPaymentsPoller
  2. For each poller, implement a property to enable or disable the poller. For example, (in Java):
    Copy
    Copied!
    @ConditionalOnProperty(value = "ripplenet.poller.accepted.enabled", matchIfMissing = false, havingValue = "true")
  3. Implement a property to define an initial delay after startup (including a default value as well):
    Copy
    Copied!
    @Scheduled(..., initialDelayString = "${ripplenet.poller.accepted.initial.delay:30000}")
  4. Implement a property to define a fixed delay between two executions of the same poller (including a default value as well). The fixedDelay property makes sure that there is a delay of n millisecond between the time a poller finishes executing, and the start time of the next execution of the same poller:
    Copy
    Copied!
    @Scheduled(fixedDelayString = "${ripplenet.poller.accepted.fixed.delay:30000}", ...)
  5. Finally, include all these properties in application.properties to easily update their values without recompiling your code:
    Copy
    Copied!
    ripplenet.poller.accepted.enabled=false
    ripplenet.poller.accepted.fixed.delay=30000
    ripplenet.poller.accepted.initial.delay=30000

Design considerations

You can adjust the fixed delay between two executions of the same poller, depending on the number of payments you have to process. Here are some aspects to consider:

Design type Pros Cons
Fixed delay < 30 seconds
  • Process payments quickly
  • May overload RippleNet and your internal systems
  • If high frequency is required to process a lot of payments, consider using MQ
60 seconds > Fixed delay >= 30 seconds - Recommended
  • Good "payments processing/system load" ratio
  • Enough time to process all the payments
Fixed delay > 60 seconds
  • Enough time to process all the payments
  • Processes payments more slowly

Error handling

Here are some considerations with respect to handling HTTP errors:

HTTP errors

Copy
Copied!
GET /v4/payments
HTTP Status Code Error Message Description
400 invalid_request Invalid Request parameters
401 Error 401 Unauthorized Bad credentials, wrong authentication method, OR the token may be expired
403 Forbidden No handler found for GET
500 Server not available Service is temporarily unavailable.

Handlers

Name Code Action
Unauthorized 401 Payments will stop, throw an alert. Credentials could have been altered by an administrator or renew the token.
Server not available 500 Payments will stop. Retry three times every two seconds. If it fails, raise an alert.

Handlers sample code

To handle retries with a 500 HTTP status code, the RippleNet Reference Implementation uses the Spring @Retryable annotation.

Copy
Copied!
@Retryable(value = {RippleNetProblemTemporaryException.class},
            maxAttempts = 3,
            backoff = @Backoff(maxDelay = 2000, random = true))
    Payments getPayments(GetPaymentsParams getPaymentsParams)
            throws RippleNetProblemException, RnAuthException;