CREATE A PAID KEAP ORDER BY API, AND WHY YOUR AUTOMATION NEVER FIRED
You created the order by API. It shows up in Keap. The customer's purchase automation never ran, and the revenue report does not know the sale exists. An order is a receivable. The money is a second call.
THE ORDER IS THERE AND NOTHING HAPPENED
Here is how it arrives. A developer, or somebody with a Zapier account and a free afternoon, wires a checkout to Keap. Every sale creates an order by API. It works on the first try. The order appears on the contact record with the right product and the right total, and everybody moves on.
Weeks later the questions start. The welcome sequence is not going out. The sales report says the month was flat when the bank statement says otherwise. A customer emails to say they paid and never got the email with the login details. Somebody checks the integration logs and every call returned 201. Keap support says the API is working, and they are right. It is.
The orders were never paid. Not in the sense that money did not change hands. In the sense that Keap was never told. Every one of them is sitting at DRAFT with nothing against it, and everything in Keap that keys on money, which is the reports and the purchase goals, has been ignoring them since day one.
This guide walks the chain that actually records a sale, with the responses we measured at each step, then goes through the traps that sit beside it. The chain is two calls long. The traps are what cost the afternoon.
AN ORDER IS A RECEIVABLE, NOT A SALE
Keap has no separate invoice object. An order is the invoice, and creating one is the same as raising an invoice on paper: it says somebody owes you money. It does not say they paid.
We proved this on a sandbox app by creating an order against a product and reading the response, then reading the order back. The create body is the usual shape: a contact, and line items that reference a product id.
POST /crm/rest/v1/orders
{
"contact_id": 12345,
"order_items": [ { "product_id": 678, "quantity": 1 } ]
}
// what came back, on order 43126
{
"id": 43126,
"status": "DRAFT",
"total_paid": 0,
"total_due": 49
}
status: DRAFT, total_paid: 0. That is a receivable. It will stay a receivable for ever, because
there is nothing in the create body that records money, and reading the docs again will not find one. Keap's own
reports count paid money, and the Product Purchase goal in Campaign Builder listens for paid invoices. A DRAFT order
trips neither, and it is not supposed to.
This is the entire bug. Everything else in this guide is detail. If your integration creates orders and never records payments, you have built an invoicing system and called it a checkout. The fix is one more call, and the rest of the page is about making that call correctly.
THE SECOND CALL, AND THE FIELD THAT IS NOT CALLED AMOUNT
Revenue is recorded by posting a payment against the order. That is the call that flips the status, populates
total_paid, puts the sale in the reports and, as the next section shows, fires the automation.
POST /crm/rest/v1/orders/43126/payments
{
"payment_amount": 49, // payment_amount. NOT amount.
"charge_now": false // the money was taken elsewhere
}
// answered with a Successful Transaction
Two things in that body will bite you, and both are quiet about it.
The field is payment_amount. Send amount, which is what every other payment API you
have ever used called it, and Keap answers {"message":"Payment Amount is invalid"}. Read that again. It
names the field it did not receive, in a sentence that sounds like a complaint about the value. So you check the
number, you check whether it wants cents, you check whether it wants a string, and you try a smaller number in case
there is a cap. The number was never the problem. Rename the key.
Set charge_now to false whenever the payment already happened somewhere else, which for an
integration is always. With it true, Keap treats the call as an instruction to collect the money and tries to charge
a card on file. Your customer paid on your checkout an hour ago. Charging them again from their CRM record is not the
kind of automation anybody asks for.
The sandbox run answered Successful Transaction, and the order's state changed in the way the next step
reads back. On a sale you took elsewhere that is the whole call. Nothing more to send.
READ IT BACK, AND DO NOT READ TOTAL_DUE AS A BALANCE
Fetch the order again and it has changed. Status is PAID, total_paid now equals the total.
And total_due has not moved.
GET /crm/rest/v1/orders/43126
{
"id": 43126,
"status": "PAID",
"total_paid": 49,
"total_due": 49 // still 49. It was 49 before the payment too.
}
We measured this twice, on two different apps, because the first time we assumed we had done something wrong. On the
first sandbox: total 3497, total_paid 3497 after the payment, total_due still 3497. On the second:
49, 49, and 49. Same shape on both. total_due is not an outstanding balance, whatever the name says, and
it never becomes one.
So compute the balance yourself: total minus total_paid. If you show a customer what they owe, that is the
number. If you show them total_due, you are telling every paid customer they still owe the full amount, and
you will hear about it.
The reverse failure exists in the wild too. On one live account we found 18 of 27 PAID orders carrying
total_paid: 0, going back to 2015. Payments were taken on an outside merchant account and the status was set
by hand, with no payment record in Keap. So the rule for reading orders is: PAID with an empty rollup means
total is the amount, and only orders in a status other than PAID need total_paid as proof that money
arrived. The write off case, where total_paid is full and the money never came, is its own guide, and it is
next in this series.
WATCH THE GOAL FIRE, WITH NO GOAL CALL ANYWHERE
This is the part that turns the second call from bookkeeping into the thing the customer actually cares about.
We set up a campaign in the sandbox with a Product Purchase goal on a product called Pants, wired to apply a tag called
pants purchased. Then we ran the chain above on a fresh order, number 43128, and watched the contact in two
deliberate steps, because the safety net is the half worth proving.
// order 43128 created, status DRAFT
// twelve seconds later: contact has NO new tag
// POST /v1/orders/43128/payments, status now PAID
// roughly twenty seconds later: contact carries "pants purchased"
// number of API goal calls made: zero
Two things came out of that, and both matter more than they look.
A goal set to paid invoices genuinely cannot be tripped by a receivable. The DRAFT order sat there for twelve seconds and nothing happened. That is not lag. That is the goal doing exactly what it says, and it is why an integration that only creates orders produces a customer who never gets the welcome email. Keap was not told anyone paid.
The payment call fires the automation on its own. No API goal, no tag applied by your code, no HTTP POST from the campaign. The payment landed, Keap processed the goal, and the sequence started. The payment call is load bearing for automation, not just for the revenue number.
That is also why we prefer a Product Purchase goal over the other common pattern, where the integration applies a purchase tag itself and the campaign listens for the tag. The goal keys on the product, not the stock keeping unit (SKU), so it is immune to the missing SKU problem that turns up on every second account. A draft cannot fire it. It is native Keap logic on native Keap data, so it survives you replacing the integration next year. And nobody has to remember to apply the tag, which is the step that gets forgotten.
Goal processing is not instant. Twelve seconds of nothing on the draft, about twenty seconds from payment to tag. Nothing in your code may treat the payment response as confirmation that the automation ran. If you need to know it ran, read the contact back later, or subscribe to the tag applied hook and wait for it to arrive.
THE 201 THAT ACHIEVED NOTHING
Some integrations do call a goal directly, through the Campaign Builder API goal, and that path has a trap of its own which belongs beside the one above because it produces the same symptom.
Achieve an API goal that no campaign is listening for and Keap answers HTTP 201. Created. The body says otherwise.
HTTP/1.1 201 Created
[
{
"success": false,
"message": "No Goals were configured to be achieved by this event."
}
]
A created status for an event that did nothing. Every HTTP client you own will call that a success, and an implementation that checks the status code and moves on reports success forever while the customer's automation never runs. It is the failure nobody notices until a customer misses an email they were expecting, which is the same afternoon this guide started with.
Read the body. The response is an array, so loop it, and treat success: false on any element as a failure
worth logging with the message. The message is at least honest: it tells you that the campaign side was never
configured, which usually means the goal name or integration name in the campaign does not match what you sent.
DOLLARS ON V2, CENTS ON V1, ONE PATH SEGMENT APART
Keap has two REST generations and they disagree about money. The v1 order totals in the responses above are in cents.
The v2 payment endpoint takes dollars. They are one path segment apart, /crm/rest/v1/ and
/crm/rest/v2/, and neither response says which convention it is holding.
POST /crm/rest/v2/orders/{id}/payments
{
"payment_amount": 49.00, // DOLLARS. Math.round(cents) / 100 before sending.
"payment_method_id": "...", // from Keap's hosted card component
"apply_to_commissions": true,
"payment_time": "2026-08-17T14:02:11Z",
"notes": "Website checkout"
}
// the response carries transaction_id and payment_status_message.
// treat the charge as FAILED when transaction_id is absent,
// or the message matches /declin|fail|error|void/i, whatever the status code says.
The second half of that snippet is the sharper one. A 200 on the v2 payment call can still be a decline.
The HTTP status tells you Keap accepted the request; whether the card went through is in
payment_status_message, and a real transaction has a transaction_id. The checkout we run in
production checks both before it tells the customer anything, and it has been right to.
The same unit split turns up when you read a payment ledger. GET /v1/orders/{id}/payments answers in
dollars while the order it belongs to reports its totals in cents, on the same app, one request apart. Any code that
compares the two has to convert first, and code that does not will conclude that every order is a hundred times
underpaid.
YOU CAN CREATE THE PRODUCT. YOU CANNOT CREATE THE CAMPAIGN
Once orders and payments work, the next request is always the same: automate the whole setup. New product, new tag, new automation cloned from the master template. Two of those three are possible.
We measured which by probing each endpoint with an empty body. An empty body creates nothing and makes the endpoint answer for itself, because a validating endpoint tells you what it wanted, and a missing one tells you it is missing.
POST /crm/rest/v1/products {}
// 400 product_name is invalid so it exists, and validates
POST /crm/rest/v1/products/{id}/subscriptions {}
// 400 cycle is invalid so it exists, and validates
POST /crm/rest/v1/campaigns {}
// 405 Method Not Allowed no create, no clone
GET /crm/rest/v1/campaigns
// 200
Products can be created. Subscription plans can be created, nested inside a product, which is where Keap keeps them.
Campaigns cannot. POST /v1/campaigns is 405 while GET on the same path is 200, and there is no clone
endpoint anywhere. A campaign is built in Campaign Builder by a person.
That is a product constraint rather than a detail. A client whose workflow is "every new trip gets a tag, a product and a copy of the master automation" can have the tag and the product created for them, and still has to clone the campaign by hand in Keap. Say so before you quote the work. Discovering it after promising full automation is a conversation nobody enjoys.
WHEN A CUSTOMER SAYS I NEVER GOT THE EMAIL
This is the support ticket the whole guide exists for, so here is the order to check things in.
Open the order on their contact record. If it reads DRAFT with total_paid 0, stop looking at the
campaign. The campaign is fine. The integration made the first call and never made the second, and Keap has no idea
this person paid.
Record the payment now, with payment_amount and charge_now: false, so Keap does not
try to bill them a second time. Then wait. Roughly twenty seconds later the goal fires, the tag lands and the sequence
starts. The customer gets the email they were promised, late, and you did not have to touch the campaign.
Then find everyone else in the same state. Do not go looking for an invoices endpoint:
/crm/rest/v1/invoices and /crm/rest/v2/invoices both answer 404. Keap's invoices are its orders,
which is why the payment call lives under orders and why the payment webhook is called invoice.payment.add.
Query orders, and be careful how you filter, because the list has a parameter that works and one that only looks like
it does. We measured both on 100 row samples from a live account.
GET /crm/rest/v1/orders // PAID 94, VIEWED 3, DRAFT 3
GET /crm/rest/v1/orders?paid=false // 38 rows: DRAFT 21, SENT 12, VIEWED 5. A real filter.
GET /crm/rest/v1/orders?paid=true // 100 rows, all PAID
GET /crm/rest/v1/orders?status=UNPAID // full of PAID rows. Silently ignored.
paid=false is real. status=UNPAID returns 200 and does nothing, which is the dangerous kind of
failure: an integration built on it looks correct and returns the wrong rows for ever. Note that paid=false
includes DRAFT alongside SENT and VIEWED, so a "what does this customer owe" screen has to drop the drafts itself. A
draft is a receivable nobody has been asked to pay yet, and an integration bug is not an invoice.
Going forward, Keap's REST hooks make this event driven. GET /crm/rest/v1/hooks/event_keys lists 47 keys,
and order.add and invoice.payment.add are both on it. An order.add never followed by
an invoice.payment.add for the same order is your draft, and a hook consumer can flag it within a minute
instead of at month end. One measured detail for whoever wires it: a delivery's object_keys is an array, so
one delivery can carry several events and the consumer must loop, never index.
Then fix the integration. Check that the campaign is published while you are there, because the campaign
endpoint reports published_status and an unpublished campaign fires nothing either. And if the integration
calls an API goal, read the response body for success: false before believing the 201.
One call short is the whole story. The order was never wrong. It was just never paid.
COMMON QUESTIONS
POST /v1/orders lands at DRAFT
with total_paid 0 every time, and nothing in the create body changes that. Revenue takes a second call,
POST /v1/orders/{id}/payments, which flips it to PAID. An integration that only makes the first call
fills Keap with unpaid invoices that every report and every purchase goal ignores.amount. The endpoint wants payment_amount.
The error names the field it did not get, in words that sound like a complaint about the value, so people go hunting
for a rounding or currency bug. The number was fine. Rename the key.total_due is not an outstanding balance. Measured on two apps: after a full payment
the status flips to PAID, total_paid rises to the total, and total_due stays exactly where it
started. Compute the balance as total minus total_paid, and never show total_due to a customer
as what they owe.success: false and "No Goals were configured to be achieved by this event."
Read the body. Trusting the status code reports success forever while the automation never runs.POST /v1/campaigns is 405 while GET is 200, and there is no clone endpoint anywhere.
Products and subscription plans can be created by API and validate their bodies, tags can be created and applied, but
a campaign is built in Campaign Builder by a person. Plan the work around that instead of promising otherwise.Every response on this page came from a real app, and the afternoon it cost was ours. We have been connecting Keap to the rest of the stack since 2010. If your orders are in Keap and your revenue is not, or a customer's welcome email never went out and nobody can say why, that is a conversation worth having.
Book a Free Consult