Creating and Submitting Payments
Create a payment for a payee, preview its fees, submit it, and edit or cancel it before it is sent.
A payment is one payout to one payee, from one of your ledger accounts. You create it as a draft, optionally preview what it will cost, then submit it. This page walks through each step and the rules for changing a payment once it exists.
Before you start
- You have a batch. See Batches.
- You know which ledger account pays. See Funding and Ledger Accounts.
- Creating, editing and cancelling need the Write permission. Submitting and auto-submit need the Payment Approval permission. See Permissions.
Step 1: Create the payment
curl -X POST https://api-sandbox.paymentlabs.io/batches/B-01JDA1B2C3D4E5F6G7H8J9K0M1/payments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"payeeEmail": "[email protected]",
"payeeReferenceId": "user-1001",
"sourceAmount": "250.00",
"ledgerAccount": "usd-main",
"referenceId": "invoice-4471",
"payeeMemo": "October royalties",
"payorMemo": "Contract 88-B"
}'const response = await fetch(
`https://api-sandbox.paymentlabs.io/batches/${batchId}/payments`,
{
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({
payeeEmail: "[email protected]",
payeeReferenceId: "user-1001",
sourceAmount: "250.00",
ledgerAccount: "usd-main",
}),
},
);
const payment = await response.json(); // 201: payment.id is "P-...", payment.status is "DRAFT"Create Payment returns 201 with the payment and a Location header.
| Field | Required | Description |
|---|---|---|
sourceAmount | Yes | The amount to pay, as a positive decimal string in the ledger account's currency, for example "250.00". A zero amount is accepted but the payment can never be submitted. |
payeeEmail, payeeReferenceId | At least one | Who to pay. A new email creates the payee. See Payee Reference IDs for every combination. |
ledgerAccount | When you have more than one ledger account | The id or alias of the ledger account that pays. The payment's currency is that account's currency. |
paymentCategory | No | Defaults to the batch's defaultPaymentCategory. |
taxJurisdiction | No | Defaults to the batch's jurisdiction. Must be enabled on your account, or none. |
payeeMemo | No | A note the payee sees. Defaults to the batch name. In sandbox it can also choose the test outcome. |
payorMemo | No | A note only you see. |
referenceId | No | Your own label for the payment, for example your invoice number. It is not checked for uniqueness. |
tags | No | [{ "key": "region", "value": ["emea"] }]. Only accepted when tags are enabled on your account. |
You do not choose how the payee is paid. The payee picks their payout method during onboarding, within the methods your batch allows.
What happens on create:
- The payment starts in
DRAFT. The payment's service fee is charged in the background. - The payee is emailed about the payment. A payee who has not onboarded yet gets an invitation to sign up instead.
- The
payment-createdwebhook fires.
Create Payment is not idempotent. Retrying a request that timed out creates a second payment and emails the payee again. Before you retry, search for your
referenceIdwith Search Payments.
Step 2: Preview the cost (optional)
Submit Payment Dry Run calculates fees, exchange rate and tax withholding for the payment as if you submitted it now. It changes nothing, and any payout role can call it. Send the same feeCap and fxDifferenceCoverage you plan to submit with.
{
"isRetry": false,
"sourceAmount": { "value": 250, "currency": "USD", "formattedValue": "$250.00", "digits": 2, "wholeValue": 25000 },
"destinationAmount": { "value": 231.5, "currency": "EUR", "formattedValue": "€231.50", "digits": 2, "wholeValue": 23150 },
"fxRates": { "fees": 1, "destination": 0.926 },
"payor": {
"feesAmount": { "value": 5, "currency": "USD", "formattedValue": "$5.00", "digits": 2, "wholeValue": 500 },
"totalAmount": { "value": 255, "currency": "USD", "formattedValue": "$255.00", "digits": 2, "wholeValue": 25500 }
},
"payee": {
"feesAmount": { "value": 0, "currency": "USD", "formattedValue": "$0.00", "digits": 2, "wholeValue": 0 },
"taxWithholdingAmount": { "value": 0, "currency": "USD", "formattedValue": "$0.00", "digits": 2, "wholeValue": 0 },
"totalDestinationAmount": { "value": 231.5, "currency": "EUR", "formattedValue": "€231.50", "digits": 2, "wholeValue": 23150 }
}
}The example is shortened. payor.totalAmount is what leaves your ledger account (amount plus your share of the fees). payee.totalDestinationAmount is what the payee receives in their currency. The dry run returns 422 with code: NO_VALID_PAYMENT_METHOD when the payee has no usable payout method yet.
Step 3: Wait for the payee, or use auto-submit
A payment can only be sent once its payee is payable. Read isSubmittable on the payment: it is true when the payment can be submitted right now. You have two options:
- Wait for the
payee-payability-changedwebhook, then submit. - Auto-submit: submit now with
autoSubmit: true. If the payee is payable the payment is sent immediately. If not, it moves toWAITING_PAYABILITYand is sent automatically the moment the payee becomes payable, with the samefeeCapandfxDifferenceCoverage.
Step 4: Submit the payment
curl -X POST https://api-sandbox.paymentlabs.io/batches/B-01JDA1B2C3D4E5F6G7H8J9K0M1/payments/P-01JDA3C4D5E6F7G8H9J0K1M2N3/submit \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "autoSubmit": true, "feeCap": "5.00" }'Submit Payment returns 202 Accepted: the payment is queued and its status becomes SUBMITTED. The body takes the same optional fields as Submit Batch: autoSubmit, feeCap and fxDifferenceCoverage (see Batches and Payment Methods and Costs).
You can submit a payment that is DRAFT, FAILED or RETURNED. Before it queues the payment, Submit Payment checks that your ledger account balance covers sourceAmount.
Other ways to submit:
- Every payment in a batch at once: Submit Batch.
- Up to 50 payments from any of your batches: Submit Payments with
{ "payments": [{ "batchId": "B-...", "paymentId": "P-..." }] }. Always sendpayments, with 1 to 50 items. It returns200with one result per payment (success, anderrorCodeon failure), so check each one. - Turn on auto-submit without other options: Turn On Auto Submit.
A submitted payment moves money. Once it reaches
PROCESSINGit cannot be stopped.
From here the payment moves through its statuses on its own. See Payment Statuses.
Turn auto-submit off
Remove Auto Submit takes a payment out of WAITING_PAYABILITY and back to DRAFT (or to RETURNED, if it had been returned before). It returns 204. It only works while the payment is waiting for its payee; once the payment is being sent it returns 409 STILL_AUTO_SUBMITTING.
Edit a payment
Update Payment changes sourceAmount, referenceId, paymentCategory, taxJurisdiction and tags on a payment that is still DRAFT. tags replaces the whole tag list.
Edits are allowed until the end of the payment's grace period: by default 5 days after it was created, unless Payment Labs set a different period for your account. After that, and in any status other than DRAFT, the payment returns 423 NOT_EDITABLE. Read isEditable on the payment before you try.
To change only the tags, in any status, use Update Payment Tags with { "tags": [...] }.
Cancel a payment
Cancel Payment cancels a payment that has not been sent: one in DRAFT, FAILED, RETURNED, WAITING_PAYABILITY or RESTRICTED, and not already queued. You can send an optional { "reason": "Duplicate invoice" }.
{ "message": "Payment P-01JDA3C4D5E6F7G8H9J0K1M2N3 has been cancelled.", "refundableServiceFee": true }- The response is
202. The payment showsCANCELLEDimmediately and the cancellation completes in the background;payment-status-changedfires when it does. refundableServiceFeeistruewhen you cancelled within the grace period, in which case the service fee is refunded to you.isCancellableon the payment tells you whether cancelling is possible now.- A cancelled payment cannot be reopened. Create a new one.
Virtual payments
A virtual payment records a payment to a payee without sending money to their payout method. Use it when the money stays inside Payment Labs, for example to credit a wallet, but you still need the payment for tax reporting and withholding.
- Create it with Create Virtual Payment. The body is the same as Create Payment. It appears in the batch with
virtualPayment: true. - Submit, edit and cancel it with the same operations as a normal payment.
- The payee must still be payable. No payout method is used, and only tax withholdings and fees leave your ledger account.
- Submit checks that your ledger account balance covers the full
sourceAmount, as for a normal payment. - Webhooks for a virtual payment fire only when it reaches
COMPLETED.
Errors and edge cases
| Error | Operation | Meaning | What to do |
|---|---|---|---|
400 VALIDATION_ERROR | Create, Edit | A field is invalid or not accepted. For ledgerAccount, see the ledger account errors. | Fix the field named in details. |
400 REQUIRED_VALUES_MISSING, REFERENCE_ID_NOT_FOUND, EMAIL_ALREADY_IN_USE | Create | The payee could not be matched. | See Payee Reference IDs. |
404 NOT_FOUND | All | The batch or payment does not exist or is not yours. | Check the IDs. |
423 NOT_EDITABLE | Create, Submit | The batch is archived. | Unarchive it. |
423 NOT_EDITABLE | Edit | The payment is not DRAFT, or its grace period is over. | Cancel it and create a new one. |
423 NOT_EDITABLE | Cancel | The payment is already being sent, or was already completed or cancelled. | Nothing to cancel. |
423 INSUFFICIENT_BALANCE | Submit | The ledger account balance is below sourceAmount. | Fund the account, then submit again. |
409 | Submit | The payment is not DRAFT, FAILED or RETURNED; it is already queued or being cancelled; or the payee is not payable and you did not send autoSubmit: true. | Read the payment. Use autoSubmit: true for payees who are still onboarding. |
409 CUSTOMER_FROZEN | Submit | Payouts from your account are frozen. | Contact Payment Labs. |
403 FORBIDDEN_CONDITION_NOT_MET | Submit | The amount is above your role's approval limit. | Use a credential without the limit. |
409 STILL_AUTO_SUBMITTING | Remove auto-submit | The payment is already being sent. | Wait for its next status. |
409 NOT_WAITING_PAYABILITY, 423 | Remove auto-submit | The payment is not waiting for its payee, or auto-submit is not on. | Nothing to remove. |
422 NO_VALID_PAYMENT_METHOD | Dry run | The payee has no usable payout method yet. | Wait for the payee to add one. |
Bulk submit reports the same conditions per payment as errorCode values: PAYMENT_NOT_FOUND, BATCH_NOT_FOUND, BATCH_ARCHIVED, PAYMENT_NOT_SUBMITTABLE, PAYEE_NOT_PAYABLE, INSUFFICIENT_BALANCE, PAYOR_BLOCKED, CUSTOMER_FROZEN, PAYMENT_ALREADY_SUBMITTED (the same payment twice in one request) and SERVER_ERROR (retry that payment). More than 50 payments returns 400 VALIDATION_ERROR with details.payments[0].code = TOO_MANY_PAYMENTS.
Next steps
Updated 1 day ago
What’s Next
Next: Payment Statuses.
