Collect a Manual Entry Payment

Manual entry payments let merchants collect credit card payments through their own application using Podium's payment infrastructure

Manual entry payments let merchants collect credit card payments through their own application using Podium's payment infrastructure. Instead of using a physical card reader, the customer's card details are securely collected via Stripe.js on the client side — card numbers never touch Podium or your servers, maintaining PCI compliance.

Use this flow when:

  • Building a custom checkout experience in a merchant-facing application
  • Accepting payments where a physical card reader is not available
  • Integrating card-on-file or keyed-in payment capabilities

What you'll need

  • A valid OAuth token with the write_payments scope
  • The locationUid for the Podium location processing the payment
  • Stripe.js loaded on your client-side application (Stripe.js docs)
  • The Podium API base URL: https://api.podium.com

How it works

The manual entry payment flow has four steps:

  1. Create an invoice — Register the payment amount and line items with Podium
  2. Create a SetupIntent — Get a Stripe clientSecret to securely tokenize the card
  3. Collect card details — Use Stripe.js on the client to capture card information (PCI-compliant)
  4. Charge the invoice — Submit the tokenized paymentMethodId to Podium to process the payment

Step 1: Create an invoice

Create an invoice with one or more line items. The total amount is the sum of all line item amounts, in cents.

Request

POST /v4/invoices

Headers

HeaderValue
AuthorizationBearer {oauth_token}
Content-Typeapplication/json

Body parameters

ParameterTypeRequiredDescription
locationUidstringYesUUID of the Podium location
customerNamestringYesName of the customer being charged
channelIdentifierstringYesCustomer contact (e.g. phone number or email)
invoiceNumberstringYesYour reference number for this invoice
lineItemsarrayYesOne or more line items (min 1, max 10)
lineItems[].amountintegerYesAmount in cents (total across items must be 50–99,999,900)
lineItems[].descriptionstringYesDescription of the line item (max 50 characters)
accountUidstringNoUUID of an existing Podium contact
allowedPaymentOptionsarrayNoRestrict payment methods: "creditCard", "debitCard", "bankAccount", "buyNowPayLaterAffirm"

Example

curl --request POST \
  --url https://api.podium.com/v4/invoices \
  --header 'Authorization: Bearer {oauth_token}' \
  --header 'Content-Type: application/json' \
  --data '{
    "locationUid": "585e3541-a196-4956-a787-b5f0c1ccdd97",
    "customerName": "Jane Smith",
    "channelIdentifier": "+18015551234",
    "invoiceNumber": "INV-001",
    "lineItems": [
      {
        "amount": 5000,
        "description": "Consultation fee"
      }
    ]
  }'

Response

{
  "data": {
    "uid": "f7e6d5c4-b3a2-1098-7654-321fedcba098",
    "status": "created",
    "amount": 5000,
    "customerName": "Jane Smith",
    "invoiceNumber": "INV-001",
    "currencyRef": "USD",
    "createdAt": "2026-02-19T12:00:00Z",
    "lineItems": [
      {
        "amount": 5000,
        "description": "Consultation fee"
      }
    ],
    "location": {
      "uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
    },
    "allowedPaymentMethods": ["creditCard"],
    "payments": []
  },
  "metadata": {}
}

Save the uid from the response — this is your invoiceUid for the remaining steps.


Step 2: Create a SetupIntent

Request a Stripe SetupIntent for the invoice. This returns a clientSecret that you'll use on the client side to securely collect card details.

Request

POST /v4/invoices/{invoiceUid}/setup

Headers

HeaderValue
AuthorizationBearer {oauth_token}
Content-Typeapplication/json

Body parameters

ParameterTypeRequiredDescription
locationUidstringYesUUID of the Podium location (must match the invoice)

Example

curl --request POST \
  --url https://api.podium.com/v4/invoices/f7e6d5c4-b3a2-1098-7654-321fedcba098/setup \
  --header 'Authorization: Bearer {oauth_token}' \
  --header 'Content-Type: application/json' \
  --data '{
    "locationUid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }'

Response

{
  "data": {
    "uid": "f7e6d5c4-b3a2-1098-7654-321fedcba098"
  },
  "metadata": {
    "setupIntent": {
      "clientSecret": "seti_1234567890_secret_abcdefghijklmnop"
    },
    "isTest": true,
    "stripeCustomerId": "cus_1234567890"
  }
}

You'll need three values from this response:

FieldUsage
metadata.setupIntent.clientSecretPass to Stripe.js to collect card details
metadata.stripeCustomerIdThe Stripe customer associated with this payment
metadata.isTestWhether this is a test-mode transaction

Step 3: Collect card details with Stripe.js (client-side)

This step happens entirely on the client side. Use Stripe.js to collect the card details and confirm the SetupIntent. Card numbers are sent directly to Stripe and never pass through your servers or Podium's — keeping you PCI compliant.

Important: You must use the merchant's Stripe publishable key to initialize Stripe.js. If isTest is true, use the test publishable key.

The setupIntent.payment_method value (e.g. pm_1234567890) is the paymentMethodId you'll send to Podium in the next step.


Step 4: Charge the invoice

Submit the paymentMethodId from Step 3 to charge the invoice. This is the final step that processes the payment.

Request

POST /v4/invoices/{invoiceUid}/charge

Headers

HeaderValue
AuthorizationBearer {oauth_token}
Content-Typeapplication/json

Body parameters

ParameterTypeRequiredDescription
locationUidstringYesUUID of the Podium location
paymentMethodIdstringYesThe Stripe payment method ID from Step 3 (e.g. pm_1234...)
idempotencyUidstringNoUUID to prevent duplicate charges. Strongly recommended.
shouldSaveCardOnFilebooleanNoSave this card for future payments. Defaults to false.

Example

curl --request POST \
  --url https://api.podium.com/v4/invoices/f7e6d5c4-b3a2-1098-7654-321fedcba098/charge \
  --header 'Authorization: Bearer {oauth_token}' \
  --header 'Content-Type: application/json' \
  --data '{
    "locationUid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "paymentMethodId": "pm_1234567890",
    "idempotencyUid": "d4e5f6a7-b8c9-0123-4567-89abcdef0123"
  }'

Response

{
  "data": {
    "uid": "f7e6d5c4-b3a2-1098-7654-321fedcba098",
    "status": "paid",
    "amount": 5000,
    "customerName": "Jane Smith",
    "invoiceNumber": "INV-001",
    "paidAt": "2026-02-19T12:01:30Z",
    "payments": [
      {
        "uid": "ab12cd34-ef56-7890-abcd-1234567890ab",
        "status": "succeeded",
        "cardBrand": "visa",
        "cardLast4": "4242",
        "cardFunding": "CREDIT",
        "createdAt": "2026-02-19T12:01:30Z"
      }
    ]
  },
  "metadata": {
    "paymentAttempt": {
      "paymentIntentId": "pi_1234567890",
      "status": "succeeded",
      "declineCode": null,
      "declineCodeMessage": null
    }
  }
}

Check the metadata.paymentAttempt.status to confirm the result:

StatusMeaning
succeededPayment was successful
requires_actionAdditional authentication needed (e.g. 3D Secure)
canceledPayment was canceled

Tip: Always pass an idempotencyUid when charging. If a network issue causes a timeout, you can safely retry the same request without risking a double charge.


Error handling

All error responses follow this format:

{
  "code": 400,
  "message": "Description of the error."
}