Pepvote Support Center

Payments

Link a payment business and handle checkout in a public voting interface.

Use these organizer endpoints from your server with a read or write key. There is no API endpoint to create a payment business. Connect and complete the business setup in Pepvote first.

Use the public payment endpoint from your server with a vote key when you need a voter-facing payment status.

Endpoints

MethodPathScopeDescription
GET/payment-businessesreadList your payment businesses.
GET/voting-events/:idOrCode/payment-businessreadGet the business linked to an event.
PUT/voting-events/:idOrCode/payment-businesswriteLink a business to an event.
DELETE/voting-events/:idOrCode/payment-businesswriteUnlink the business from an event.
GET/voting-events/:idOrCode/payments/:idreadGet an organizer payment attempt.
GET/public/voting-events/:idOrCode/payments/:idvoteGet a voter payment attempt.

Payment businesses

GET /payment-businesses returns businesses you can use. Each business includes supportedCurrencies. Use only one of these currencies when you set pricing.

curl "https://api.pepvote.com/payment-businesses" \
  -H "Authorization: Bearer $PEPVOTE_API_KEY" \
  -H "User-Agent: my-app/1.0"
{
  "data": [
    {
      "id": "j57business01",
      "provider": "stripe",
      "displayName": "Northstar Ltd",
      "country": "US",
      "ready": true,
      "supportedCurrencies": [
        "usd",
        "cad"
      ]
    }
  ],
  "nextCursor": null
}

Link one ready business before you set paid pricing. A business can be unlinked later. The event must have the matching pay-to-vote or pay-to-register-candidate setting and plan before it can use checkout.

curl -X PUT "https://api.pepvote.com/voting-events/BOARD26/payment-business" \
  -H "Authorization: Bearer $PEPVOTE_API_KEY" \
  -H "User-Agent: my-app/1.0" \
  -H "Content-Type: application/json" \
  -d '{"paymentBusinessId":"j57business01"}'
{
  "data": {
    "id": "j57business01",
    "provider": "stripe",
    "displayName": "Northstar Ltd",
    "country": "US",
    "ready": true,
    "supportedCurrencies": [
      "usd",
      "cad"
    ]
  }
}

Payment status

Poll GET /public/voting-events/:idOrCode/payments/:id from your server while your interface waits for payment. You can also wait for the payment.succeeded webhook. The organizer endpoint returns the same payment attempt for an event the organizer can access.

status moves through created, pending, succeeded, and fulfilled, or ends in failed or cancelled. amountMinor is in the currency's smallest unit (500 is $5.00), unlike the amount in a 402 response, which is in the main unit. confirmationCode is set once a paid ballot has been cast, so a client that lost the cast response can recover it here.

curl "https://api.pepvote.com/public/voting-events/POLL26/payments/j57payment01" \
  -H "Authorization: Bearer $PEPVOTE_VOTE_KEY" \
  -H "User-Agent: my-app/1.0"
{
  "data": {
    "id": "j57payment01",
    "votingEventId": "j57event01",
    "purpose": "vote",
    "status": "succeeded",
    "provider": "stripe",
    "amountMinor": 500,
    "currency": "usd",
    "confirmationCode": null,
    "createdAt": 1780000000000,
    "updatedAt": 1780000030000
  }
}

Checkout flow

Three public endpoints can require payment. Each one plays both roles: the first call starts checkout and returns 402, and a second call with only paymentAttemptId finishes it.

FlowEndpointFirst callSecond call
Pay to votePOST /public/voting-events/:idOrCode/ballot/castBallot fields plus returnUrl{ "paymentAttemptId": "..." } returns 201 with the confirmation code
Paid candidate registrationPOST /public/voting-events/:idOrCode/register/candidateCandidate fields plus returnUrl{ "paymentAttemptId": "..." } returns 201 with the candidate
Quadratic credit top-upPOST /public/voting-events/:idOrCode/quadratic-credits/checkoutPurchase fields plus returnUrl{ "paymentAttemptId": "..." } returns 200 with the new balance

The first call saves the ballot, candidate details, or credit request on the payment attempt, so the second call needs nothing else.

Both providers work the same way: send the payer to the checkoutUrl in the response. Paystack returns it as paystack.checkoutUrl; Stripe returns a hosted Checkout page as stripe.checkoutUrl. You never load provider JavaScript or handle card details.

Include returnUrl in the request body. After the payer finishes, the provider redirects them there with paymentAttemptId appended as a query parameter. returnUrl is required when the event pays through Stripe (Stripe's page needs a destination) and optional for Paystack, which falls back to the callback configured in the merchant's Paystack Dashboard. You can also send cancelUrl for Stripe; it becomes the back link on the hosted page. Both must be absolute URLs. Use https:// for a live integration.

After payment succeeds, call the endpoint that returned 402 again from your server with { "paymentAttemptId": "..." } and nothing else. Do not resend the votes, candidate details, or credit request. Poll GET /public/voting-events/:idOrCode/payments/:id or wait for the payment.succeeded webhook before that second call. The redirect alone is not proof of payment; a payer can close the tab before it fires, so the second call is what verifies and fulfils.

The 402 response

FieldTypeDescription
paymentRequiredtrueAlways true on a 402.
paymentAttemptIdstringSend this back to finish, and use it to read payment status.
amountnumberPrice in the currency's main unit, for example 5 for $5.00.
currencystringLowercase ISO code.
providerstripe or paystackWhich object below is present.
stripe.checkoutUrlstringStripe-hosted page. Valid for one hour.
stripe.checkoutSessionIdstringStripe's cs_ id, for your logs.
paystack.checkoutUrlstringPaystack-hosted page.
paystack.accessCodestringFor Paystack's inline popup, if you prefer it over the URL.
paystack.referencestringPaystack transaction reference.
credits, pricingModelnumber, stringQuadratic credit checkout only.
nextstringHuman-readable reminder of the second call.

Paystack checkout

curl -X POST "https://api.pepvote.com/public/voting-events/POLL26/ballot/cast" \
  -H "Authorization: Bearer $PEPVOTE_VOTE_KEY" \
  -H "User-Agent: my-app/1.0" \
  -H "Content-Type: application/json" \
  -d '{"visitorId":"vi_ab12ab12ab12ab12ab12ab12ab12ab12","email":"voter@example.com","returnUrl":"https://your-app.example/paid","votes":[{"positionId":"j57position01","candidateId":"j57candidate01"}]}'
{
  "data": {
    "paymentRequired": true,
    "paymentAttemptId": "j57payment01",
    "amount": 5,
    "currency": "ngn",
    "provider": "paystack",
    "paystack": {
      "checkoutUrl": "https://checkout.paystack.com/example",
      "accessCode": "access_code",
      "reference": "reference"
    },
    "next": "Send the payer to the provider checkoutUrl. After the payment succeeds, call this endpoint again with { paymentAttemptId } to finish."
  }
}

Stripe checkout

curl -X POST "https://api.pepvote.com/public/voting-events/POLL26/ballot/cast" \
  -H "Authorization: Bearer $PEPVOTE_VOTE_KEY" \
  -H "User-Agent: my-app/1.0" \
  -H "Content-Type: application/json" \
  -d '{"visitorId":"vi_ab12ab12ab12ab12ab12ab12ab12ab12","returnUrl":"https://your-app.example/paid","cancelUrl":"https://your-app.example/cancelled","votes":[{"positionId":"j57position01","candidateId":"j57candidate01"}]}'
{
  "data": {
    "paymentRequired": true,
    "paymentAttemptId": "j57payment01",
    "amount": 5,
    "currency": "usd",
    "provider": "stripe",
    "stripe": {
      "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_example",
      "checkoutSessionId": "cs_example"
    },
    "next": "Send the payer to the provider checkoutUrl. After the payment succeeds, call this endpoint again with { paymentAttemptId } to finish."
  }
}

The payer lands on https://your-app.example/paid?paymentAttemptId=j57payment01 after paying. Omitting returnUrl on a Stripe event returns 422 with field: "returnUrl".

Finish checkout

Use the same URL and send only the payment attempt ID after the provider reports success.

curl -X POST "https://api.pepvote.com/public/voting-events/POLL26/ballot/cast" \
  -H "Authorization: Bearer $PEPVOTE_VOTE_KEY" \
  -H "User-Agent: my-app/1.0" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: paid-ballot-001" \
  -d '{"paymentAttemptId":"j57payment01"}'
{
  "data": {
    "confirmationCode": "PV-9D4R",
    "votesCount": 1
  }
}

register/candidate and quadratic-credits/checkout finish the same way: the same URL, a body of only paymentAttemptId.

Was this helpful?

Last updated September 6, 2026

On this page