API Authentication

How can you authenticate with keap max classic??? I have both a personal key and a service key from my org’s keap instance, and an API key from keys.developer.keap.com. I have had success using postman and either my personal key or the service key, but when I send a POST to our instance using python on a local server, I get a 401 citing ‘invalid access token’.

the code making the call looks like:

keapurl = ‘https://api.infusionsoft.com/crm/rest/v1/files
keapfilebody = {
“file_name”: “test.txt”,
“file_data”: “RQ==”,
“contact_id”: “51677”,
“file_association”: “CONTACT”
keapheaders = {“Content-Type”: “application/json”,“X-Keap-API-Key”: kpkey,“Accept”:“/”,“Connection”: “keep-alive”}

keapresponse = requests.post(keapurl, keapfilebody, keapheaders)

Good morning James!

If the request is working in Postman then the key is valid, and the problem is likely in how the request is being sent. I’d strip out complicating factors and try to do a basic GET request to /v1/contacts using it in your code with the minimal headers: Content-Type and the X-Keap-API-Key (if that is indeed the authentication method you are using).

Also, just to clarify because you mention both types, Keap currently supports two different authentication methods, intended for different purposes:

  1. OAuth2 - For developers building large-scale integrations or those that require integrating with multiple Keap instances and end-users granting access to them. Note that this follows the IETF RFC 6749 Authorization Code three-legged grant flow specification. The Access Token is provided as a Bearer header to the server.


  1. Personal Access Tokens and Service Account Keys - The difference between the two is if they are acting as the authorizing user or as the system itself with admin access, but intended for small-scale projects such as retrieving data to fill a spreadsheet, with a much more limited number of queries per second allowed: The PAT or SAK is provided as a X-Keap-API-Key header to the server.



  • Tom Scott
    Keap API Engineer

I was able to pull a contact record without issue, but I have the same results using the same headers as those used in my GET.

Is the file upload endpoint restricted to only oauth2 auth?

Screenshot 2023-07-13 142921
Is the ‘AppID’ in the dashboard at keys.developer.keap.com the value I need to use as my Client ID for oauth2?

I got a token by supplying a bogus redirecturi to the /authorize endpoint, but if authorization_code is the only grant type keap supports, how am I supposed to integrate with keap’s rest services in a headless web service?

If you are writing a service that will connect without further user intervention either you can design a simple receiver endpoint, make it accessible to the web then use the authorization page yourself to authorize it, or you can walk the process by hand initially and manually store the results. In the response you will receive an Access Token and a Refresh Token. At any time you can exchange the Refresh Token for a new Access Token and Refresh Token pair.

The Access Token is valid for 24 hours.
The Refresh Token is valid for 45 days.

So, the pattern we see most often is that you will add the initial Refresh Token and Access Token to your service credential store, and then make calls as usual in your code. When the call fails (after 24 hours) you would just update the tokens by making the refresh request and storing the returned values, then retrying your call.

To be on the safe side, I’d also recommend storing the timestamp for each time you refresh it as well, and then have a 30-day cronjob that goes through and refreshes any tokens that haven’t been refreshed in the last 30 days. That ensures that you won’t have one “fall off” and need to be re-added manually again.

As for our REST API, the entire suite is accessible via PATs, SAKs or OAuth2.

closing the loop
After some serious debugging and testing, I’ve figured it out.

Keap’s /files endpoint will freak out if you don’t send a content length header along with auth, content-type, and host. Also, if the body of the request isn’t encoded in utf-8, you will get a 400.

Neither of these details are anywhere in the documentation.