Move money externally
curl --request POST \
--url https://api.getlemma.com/v0/ach-transfer \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--data '
{
"source_account_id": "<string>",
"destination_account_id": "<string>",
"amount": 123,
"description": "<string>"
}
'import requests
url = "https://api.getlemma.com/v0/ach-transfer"
payload = {
"source_account_id": "<string>",
"destination_account_id": "<string>",
"amount": 123,
"description": "<string>"
}
headers = {
"Idempotency-Key": "<idempotency-key>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'Idempotency-Key': '<idempotency-key>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
source_account_id: '<string>',
destination_account_id: '<string>',
amount: 123,
description: '<string>'
})
};
fetch('https://api.getlemma.com/v0/ach-transfer', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.getlemma.com/v0/ach-transfer",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'source_account_id' => '<string>',
'destination_account_id' => '<string>',
'amount' => 123,
'description' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json",
"Idempotency-Key: <idempotency-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.getlemma.com/v0/ach-transfer"
payload := strings.NewReader("{\n \"source_account_id\": \"<string>\",\n \"destination_account_id\": \"<string>\",\n \"amount\": 123,\n \"description\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Idempotency-Key", "<idempotency-key>")
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.getlemma.com/v0/ach-transfer")
.header("Idempotency-Key", "<idempotency-key>")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"source_account_id\": \"<string>\",\n \"destination_account_id\": \"<string>\",\n \"amount\": 123,\n \"description\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.getlemma.com/v0/ach-transfer")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Idempotency-Key"] = '<idempotency-key>'
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"source_account_id\": \"<string>\",\n \"destination_account_id\": \"<string>\",\n \"amount\": 123,\n \"description\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": "ach_transfer_Lk7nQx4mVp9bRt3d",
"account_id": "account_Rv4nBt8xKw2pMh6s",
"external_account_id": "external_account_Bx5nTm8hKw3pVd7c",
"amount": 150000,
"status": "submitted",
"statement_descriptor": "Acme payroll",
"transaction_id": null,
"settled_at": null,
"submission": {
"submitted_at": "2026-03-15T14:30:00Z",
"expected_funds_settlement_at": "2026-03-17T14:30:00Z"
},
"return": null
}
Outbound ACH transfers
Move money externally
Send money from a Lemma account to a saved external account over ACH, or pull money from an external account into a Lemma account.
POST
/
v0
/
ach-transfer
Move money externally
curl --request POST \
--url https://api.getlemma.com/v0/ach-transfer \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--data '
{
"source_account_id": "<string>",
"destination_account_id": "<string>",
"amount": 123,
"description": "<string>"
}
'import requests
url = "https://api.getlemma.com/v0/ach-transfer"
payload = {
"source_account_id": "<string>",
"destination_account_id": "<string>",
"amount": 123,
"description": "<string>"
}
headers = {
"Idempotency-Key": "<idempotency-key>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'Idempotency-Key': '<idempotency-key>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({
source_account_id: '<string>',
destination_account_id: '<string>',
amount: 123,
description: '<string>'
})
};
fetch('https://api.getlemma.com/v0/ach-transfer', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.getlemma.com/v0/ach-transfer",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'source_account_id' => '<string>',
'destination_account_id' => '<string>',
'amount' => 123,
'description' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json",
"Idempotency-Key: <idempotency-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.getlemma.com/v0/ach-transfer"
payload := strings.NewReader("{\n \"source_account_id\": \"<string>\",\n \"destination_account_id\": \"<string>\",\n \"amount\": 123,\n \"description\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Idempotency-Key", "<idempotency-key>")
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.getlemma.com/v0/ach-transfer")
.header("Idempotency-Key", "<idempotency-key>")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"source_account_id\": \"<string>\",\n \"destination_account_id\": \"<string>\",\n \"amount\": 123,\n \"description\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.getlemma.com/v0/ach-transfer")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Idempotency-Key"] = '<idempotency-key>'
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"source_account_id\": \"<string>\",\n \"destination_account_id\": \"<string>\",\n \"amount\": 123,\n \"description\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": "ach_transfer_Lk7nQx4mVp9bRt3d",
"account_id": "account_Rv4nBt8xKw2pMh6s",
"external_account_id": "external_account_Bx5nTm8hKw3pVd7c",
"amount": 150000,
"status": "submitted",
"statement_descriptor": "Acme payroll",
"transaction_id": null,
"settled_at": null,
"submission": {
"submitted_at": "2026-03-15T14:30:00Z",
"expected_funds_settlement_at": "2026-03-17T14:30:00Z"
},
"return": null
}
Creates a transfer from a Lemma account to a saved recipient — an external bank account registered in Lemma. External transfers are sent via ACH and typically settle within 1-2 business days.
You can also use this endpoint to pull money from an external account into a Lemma account.
Lemma checks both accounts before it sends the pull. If a check fails, Lemma moves no money and returns one of these errors:
Lemma refuses a pull with
Transfers over $100,000 are held for manual review by the Lemma team as a
security measure to protect your accounts in the event of a compromised API
key. Need higher limits? Contact us.
Headers
string
required
A key that lets you safely retry the request without sending the same ACH
transfer more than once. See Idempotency.
Request body
string
required
The ID of the Lemma account to send money from. For an ACH pull,
pass the ID of the external account to pull money from.
string
required
The ID of a saved recipient. You cannot
send money to an arbitrary bank account — it must be a pre-registered
recipient belonging to your entity. For an ACH pull, pass the ID
of the Lemma account that receives the money.
integer
required
The amount to transfer in cents. Must be greater than zero.
string
A description for the transfer. Appears on the sender’s transaction history.
ACH pulls
An ACH pull debits an external account and deposits the money into one of your entity’s Lemma accounts. To create a pull, setsource_account_id to an external account ID and destination_account_id to a Lemma account ID. Send amount as a positive number of cents. The returned transfer reports the pull with a negative amount.
Your API key needs the external_accounts / initiate_ach_pull permission on the external account. Both accounts must belong to the same entity.
Example ACH pull
curl -X POST https://api.getlemma.com/v0/ach-transfer \
-H "Authorization: Bearer your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7f3c9b2e-4a1d-4e8f-9c6b-2d5e8a1f0b3c" \
-d '{
"source_account_id": "external_account_Bx5nTm8hKw3pVd7c",
"destination_account_id": "account_Rv4nBt8xKw2pMh6s",
"amount": 150000,
"description": "Owner contribution"
}'
| Status | message | Cause |
|---|---|---|
| 400 | Destination must be a bank account for ACH pulls | destination_account_id is not a Lemma account ID. |
| 400 | Invalid bank account | The destination Lemma account is closed or unavailable, or the external account’s bank details are invalid. |
| 404 | External account not found | No external account with this ID is readable by you. |
| 422 | This external account can't be pulled from | The external account is archived or its bank details are no longer valid for pulls. See the list below. |
422 when any of these is true:
- The external account is archived.
- The account holder’s bank returned an earlier pull from this account as unauthorized.
- The bank reported the account numbers as closed or incorrect.
422 with the same external account returns the same error. Pull from a different external account instead.
Response
Returns the created ACH transfer object.string
Unique identifier for the ACH transfer.
string
The Lemma account the transfer moves money from or to.
string | null
The external account on the other side of
the transfer.
integer
The transfer amount in cents. A positive amount indicates a credit transfer
pushing funds to the receiving account. A negative amount indicates a debit
transfer pulling funds from the receiving account (an ACH pull).
string
Current status of the ACH transfer.
Show possible values
Show possible values
-
pending— in flight. -
submitted— sent to the ACH network. When the Federal Reserve settles the transfer, the status stayssubmittedandsettled_atis populated. -
canceled— we canceled the transfer after holding it for manual review. -
rejected— the ACH network rejected the transfer. -
returned— the transfer was returned.
string
The statement descriptor shown to the counterparty.
string | null
The settled transaction, once the transfer posts.
object | null
object | null
Return details, if the transfer was returned.
Show Return object
Show Return object
string
The three-character ACH return reason code.
Show ACH return reason codes
Show ACH return reason codes
| Code | Description |
|---|---|
R01 | Insufficient Funds |
R02 | Account Closed |
R03 | No Account on File |
R04 | Invalid Account Number Structure |
R05 | Unauthorized Debit to Consumer Account Using Corporate SEC Code |
R06 | Returned per ODFI’s Request |
R07 | Authorization Revoked by Customer |
R08 | Payment Stopped |
R09 | Uncollected Funds |
R10 | Customer Advises Unauthorized, Improper, Ineligible, or Incomplete |
R11 | Customer Advises Entry Not in Accordance with Terms of Authorization |
R12 | Account Sold to Another DFI |
R13 | Invalid ACH Routing Number |
R14 | Representative Payee Deceased or Unable to Continue in That Capacity |
R15 | Beneficiary or Account Holder Deceased |
R16 | Account Frozen or Entry Returned per OFAC Instruction |
R17 | File Record Edit Criteria |
R18 | Improper Effective Entry Date |
R19 | Amount Field Error |
R20 | Non-Transaction Account |
R21 | Invalid Company Identification |
R22 | Invalid Individual Identification Number |
R23 | Credit Entry Refused by Receiver |
R24 | Duplicate Entry |
R25 | Addenda Error |
R26 | Mandatory Field Error |
R27 | Trace Number Error |
R28 | Routing Number Check Digit Error |
R29 | Corporate Customer Advises Not Authorized |
R30 | RDFI Not Participant in Check Truncation Program |
R31 | Permissible Return Entry |
R32 | RDFI Non-Settlement |
R33 | Return of XCK Entry |
R34 | Limited Participation DFI |
R35 | Return of Improper Debit Entry |
R36 | Return of Improper Credit Entry |
R37 | Source Document Presented for Payment |
R38 | Stop Payment on Source Document |
R39 | Improper Source Document or Source Document Presented |
R40 | Return of ENR Entry |
R41 | Invalid Transaction Code |
R42 | Routing Number / Account Number Mismatch |
R43 | Invalid DFI Account Number |
R44 | Invalid Individual Identifier |
R45 | Invalid Individual Name |
R46 | Invalid Representative Payee Indicator |
R47 | Duplicate Enrollment |
R50 | State Law Affecting RCK Acceptance |
R51 | Item is Ineligible, Notice Not Provided |
R52 | Stop Payment on Item |
R53 | Item and ACH Entry Presented for Payment |
R61 | Misrouted Return |
R62 | Incorrect Trace Number |
R63 | Incorrect Dollar Amount |
R64 | Incorrect Individual Identification |
R65 | Incorrect Transaction Code |
R66 | Incorrect Company Identification |
R67 | Duplicate Return |
R68 | Untimely Return |
R69 | Multiple Errors |
R70 | Permissible Return Entry Not Accepted / Notice Not Provided |
R71 | Misrouted Dishonored Return |
R72 | Untimely Dishonored Return |
R73 | Timely Original Return |
R74 | Corrected Return |
R75 | Return Not a Duplicate |
R76 | No Errors Found |
R77 | Non-Acceptance of R62 Dishonored Return |
R78 | Non-Acceptance of R68 Dishonored Return |
R79 | Incorrect Data in Return Entry |
R80 | IAT Entry |
R81 | Non-Participant in IAT Program |
R82 | Invalid Foreign Receiving DFI Identification |
R83 | Foreign Receiving DFI Unable to Settle |
R84 | Entry Not Processed by Gateway |
R85 | Incorrectly Coded Outbound International Payment |
string | null
The transaction created when the transfer was returned.
{
"id": "ach_transfer_Lk7nQx4mVp9bRt3d",
"account_id": "account_Rv4nBt8xKw2pMh6s",
"external_account_id": "external_account_Bx5nTm8hKw3pVd7c",
"amount": 150000,
"status": "submitted",
"statement_descriptor": "Acme payroll",
"transaction_id": null,
"settled_at": null,
"submission": {
"submitted_at": "2026-03-15T14:30:00Z",
"expected_funds_settlement_at": "2026-03-17T14:30:00Z"
},
"return": null
}