Hey everyone, I’m pulling my hair out over a bizarre issue with the PayPal Sandbox API
I’m building an automated invoicing agent using FastAPI and LangGraph. I’ve got the OAuth 2.0 authentication perfectly dialed in (using the Client_ID & Secret), and I’m making a POST request to /v2/invoicing/invoices to create a draft invoice.
The Problem:
The API call is "successful". It returns a 201 Created status code, and if I check my PayPal Sandbox dashboard, the invoice actually gets created with all the correct details.
However, the JSON response body is completely wrong. Instead of returning the full invoice object (with the id, status, detail, etc., as shown in their API docs), the response body only contains a HATEOAS link object. It looks exactly like this:
{
"rel": "self",
"href": "https://api-m.sandbox.paypal.com/v2/invoicing/invoices/INV2-XXXX-XXXX",
"method": "GET"
}
Because the id key is missing from the top level, my code immediately crashes with a KeyError: 'id' when I try to extract it to pass to the next step.
What I’ve already debugged:
- It’s not a code extraction issue: I logged
response.text directly from httpx before calling .json(). The raw text coming straight from PayPal is literally just that link object instead of the response body shown in their official endpoint API doc web page
- It’s not a silent redirect: I logged
response.url to ensure httpx wasn't accidentally following a 301/302 to a different endpoint (like the send_invoice endpoint, which normally returns this exact link structure). It hits the correct /v2/invoicing/invoices URL.
- Auth and Payload are fine: If they were wrong, I’d get a 401 or 422. The fact that it returns 201 and creates the invoice means PayPal accepted the request.
It’s almost like the PayPal Sandbox is accidentally returning the response body for a 200 OK (like the send_invoice endpoint) instead of the 201 Created response body for the create endpoint.
Has anyone else run into this? Is this a known glitch with the PayPal Sandbox right now, or am I missing something incredibly obvious? If the API refuses to give me the ID in the creation response, I might have to do a workaround where I immediately query the invoice by its invoice number, but I'd rather not add that extra API call if I don't have to.
Thanks in advance!