Tailored audiences and ton/ta_data?


Hi there,

I’m trying to follow the flow to upload a tailored audience list through the API. The documentation for POST accounts/:account_id/tailored_audience_changes links to articles for “POST ton/ta_data” and “PUT ton/data”. But those links redirect me to https://dev.twitter.com/rest/ton, which describes endpoints based on buckets.

Is this an issue with documentation read permissions that we should reach out to our contact with? Or are these endpoints described somewhere else?

(The rest of the contracts, including the hashing format for the audience upload files themselves, is well described.)




I am seeing the same thing as William.

My guess is the bucket we are supposed to POST to is ta_data, i.e. POST /1.1/ton/bucket/ta_data?resumable=true, however whenever I issue a POST to this endpoint I get an HTTP 403 Forbidden error. If I try any other bucket value I get a HTTP 400 Bad Request error.

This doesn’t appear to be an OAuth signature issue since the response headers that I get back are showing the x-rate-limit-limit, x-rate-limit-remaining, and x-rate-limit-reset values, so it knows who I am authenticated as.

I’m guessing this is a permissions problem?


@Will_Parker @AffinioInc The bucket that you should be using is ta_partner not ta_data. This was not clear in our documentation so we’ve edited the docs to explicitly call out this bucket: https://dev.twitter.com/ads/audiences

Apologies for the confusion and thank you for bringing our attention to it!


Thanks, but I am getting “HTTP/1.1 400 Bad Request” when POST’ing to that bucket.

My request looks like:

POST /1.1/ton/bucket/ta_partner?resumable=true HTTP/1.1
Host: ton.twitter.com
Content-Length: 0
Content-Type: text/plain
X-TON-Content-Type: text/plain
X-TON-Content-Length: 130000
Authorization: OAuth oauth_consumer_key=“XXXXXXXX”, oauth_nonce=“YmFjNTg2YWE3MjNjM2M0YTQ5MzExYWQ5MWIxMDQyNDBiMjBkMWFmNA%3D%3D”, oauth_signature=“e2rseI5qVzy5hZ%2FoqVnGEq8jUfo%3D”, oauth_signature_method=“HMAC-SHA1”, oauth_timestamp=“1428521078”, oauth_token=“XXXXXXXX”, oauth_version=“1.0”

Where the XXXXXXXX are really my token and secret.

Response is

HTTP/1.1 400 Bad Request
cache-control: no-cache
content-length: 0
date: Wed, 08 Apr 2015 19:25:20 GMT
server: tsa_b
set-cookie: guest_id=v1%3A142852112007205315; Domain=.twitter.com; Path=/; Expires=Fri, 07-Apr-2017 19:25:20 UTC
strict-transport-security: max-age=631138519
x-connection-hash: d6336dbe9720fc35a7719cae20681a07
x-rate-limit-limit: 50
x-rate-limit-remaining: 45
x-rate-limit-reset: 1428521766
x-response-time: 11

Any ideas?


And I’m actually getting a 403. I’m trying to mirror the example given here: https://dev.twitter.com/rest/ton

My API call looks like:

twurl -t -H ton.twitter.com /1.1/ton/bucket/ta_partner -X POST -A "Content-Type: text/plain" -A "Content-Length: 65" --data $'85131681f483efd6111a66212d979ad3c690773c18ba12a4d608b79af6625e8c\n'

The reponse is:

-> “HTTP/1.1 403 Forbidden\r\n”
-> “connection: close\r\n”
-> “content-length: 0\r\n”
-> “date: Thu, 09 Apr 2015 15:24:40 GMT\r\n”
-> “server: tsa_a\r\n”
-> “set-cookie: guest_id=v1%3AXXXXXXredactedXXXXXXX; Domain=.twitter.com; Path=/; Expires=Sat, 08-Apr-2017 15:24:40 UTC\r\n”
-> “strict-transport-security: max-age=631138519\r\n”
-> “x-connection-hash: XXXXXXXXredactedXXXXXXXX\r\n”
-> “x-rate-limit-limit: 50\r\n”
-> “x-rate-limit-remaining: 44\r\n”
-> “x-rate-limit-reset: 1428593440\r\n”
-> “x-response-time: 10\r\n”
-> “x-tsa-request-body-time: 118\r\n”
-> “\r\n”

This same ad account can access other endpoints of the ads api using twurl, and I’ve also successfully created a tailored audience with it through the web UI, which can be read back with (twurl -H ads-api.twitter.com /0/accounts/{account_id}/tailored_audiences)

Any advice?



@AffinioInc @Will_Parker Try running this TON upload script and let me know how you fare.

@Will_Parker can you confirm that your organization has access to other write endpoints?


I’ll try to get the Perl script up and running. I’m not a Perl developer so a minimal example that works with Twurl would be the most ideal, but if the Perl version works I’m sure it’s possible to imitate the request it’s sending… I’ll let you know.

Yes I’ve created and run campaigns, uploaded app cards and media, and tracked real stats with this account, all through the API.


twurl -H ads-api.twitter.com -X POST -d "start_time=2015-04-09T00:00:00Z&funding_instrument_id={REDACTED}&name=Test Campaign 0409&total_budget_amount_local_micro=500000000&daily_budget_amount_local_micro=50000000" /0/accounts/{REDACTED}/campaigns | python -m json.tool

Yields the new campaign:

"data": {
    "account_id": "{REDACTED}",
    "created_at": "2015-04-09T18:56:40Z",
    "currency": "CAD",
    "daily_budget_amount_local_micro": 50000000,
    "deleted": false,



Hi @jillblaz

The ton_upload script is also not working.

./ton_upload -m upload -b ta_partner -f …/upload2.txt -t

opening connection to ton.twitter.com:443...
starting SSL for ton.twitter.com:443...
SSL established
<- "POST https://ton.twitter.com/1.1/ton/bucket/ta_partner HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: OAuth gem v0.4.7\r\nContent-Type: text/plain\r\nContent-Length: 130000\r\nX-Ton-Expires: Sun, 19 Apr 2015 19:09:06 GMT\r\nAuthorization: OAuth oauth_body_hash=\"kjfoiQLFrrFEUooQtXkhfkq9aVg%3D\", oauth_consumer_key=\"XXXXXXXXXXXXXX\", oauth_nonce=\"Jkzr9ajF8b0ExSBu3areEjJEm2zEEDWImERIUpNNQU\", oauth_signature=\"7d4jJsOkq6eCVAWJDFlsQWURnoY%3D\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"1428606546\", oauth_token=\"XXXXXXXXXXX\", oauth_version=\"1.0\"\r\nConnection: close\r\nHost: ton.twitter.com\r\n\r\n"

-> "HTTP/1.1 403 Forbidden\r\n"
-> "connection: close\r\n"
-> "content-length: 0\r\n"
-> "date: Thu, 09 Apr 2015 19:09:07 GMT\r\n"
-> "server: tsa_b\r\n"
-> "set-cookie: guest_id=v1%3A142860654703070563; Domain=.twitter.com; Path=/; Expires=Sat, 08-Apr-2017 19:09:07 UTC\r\n"
-> "strict-transport-security: max-age=631138519\r\n"
-> "x-connection-hash: 41d88835df5b59db33725b49a5ee031d\r\n"
-> "x-response-time: 8\r\n"
-> "x-tsa-request-body-time: 232\r\n"
-> "\r\n"
reading 0 bytes...
-> ""
read 0 bytes
Conn close


Aha, the script works!

./ton_upload --mode upload --bucket ta_partner --file test_file -t


<- "POST https://ton.twitter.com/1.1/ton/bucket/ta_partner HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: OAuth gem v0.4.7\r\nContent-Type: text/plain\r\nContent-Length: 46\r\nX-Ton-Expires: Sun, 19 Apr 2015 19:07:27 GMT\r\nAuthorization: OAuth oauth_body_hash=\"XXXXX\", oauth_consumer_key=\"XXXXX\", oauth_nonce=\"XXXXX\", oauth_signature=\"XXXXX\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"1428606447\", oauth_token=\"XXXXX\", oauth_version=\"1.0\"\r\nConnection: close\r\nHost: ton.twitter.com\r\n\r\n"

<- “66212d979ad3c690773c18ba12a4d608b79af6625e8c\n\n”
-> “HTTP/1.1 201 Created\r\n”

Now it’s a matter of imitating enough of this so it will work with our app…

Edit: The missing piece seemed to be “X-Ton-Expires” in the header. Here’s Python code (using requests-oauthlib) that uploaded successfully to /ton giving a 201 status:

    res = requests.post(
        TON_URL + '/ton/bucket/ta_partner',
            'Content-Type': 'text/plain',
            'X-Ton-Expires': 'Sun, 19 Apr 2015 19:57:30 GMT',
            settings.TWITTER_APP_KEY, settings.TWITTER_APP_SECRET, creds['accessKey'], creds['accessSecret'])



As soon as I added the X-TON-Expires I am getting

HTTP/1.1 201 Created
content-length: 0
content-type: text/csv
date: Thu, 09 Apr 2015 23:12:09 GMT
expires: Thu, 16 Apr 2015 23:12:09 UTC
location: /1.1/ton/data/ta_partner/XXXXXXXXX/DlPcAUJZBh-C3_P.csv?resumable=true&resumeId=536124
server: tsa_b
set-cookie: guest_id=v1%3A142862112915783281; Domain=.twitter.com; Path=/; Expires=Sat, 08-Apr-2017 23:12:09 UTC
strict-transport-security: max-age=631138519
x-connection-hash: dc5cf74102084c226e1bdbb108b273d4
x-rate-limit-limit: 50
x-rate-limit-remaining: 45
x-rate-limit-reset: 1428621903
x-response-time: 18
x-ton-max-chunk-size: 67108864
x-ton-min-chunk-size: 1048576


FYI @jillblaz

The documentation in the TON API for the multipart upload says I should be getting a HTTP/1.1 308 Resume Incomplete between parts, however I am actually getting HTTP/1.1 308 Permanent Redirect.

Not a big deal, but thought I’d let you know.


Arriving a bit late to this thread but I’m experiencing the same issue when uploading to TON.
I’m trying to run the Ruby script provided but I get :

lgml-ltudor3QC:ton-upload > ./ton_upload 
./ton_upload:31:in `<main>': undefined method `[]' for nil:NilClass (NoMethodError)

Looking through the comments I see the script though issues an extra OAuth header oauth_body_hash=“XXXXX” – this isn’t mentioned anywhere in the TON api. What is it? And does it have to be included in the OAuth header?


Here is a few related thread with a few you guys might find useful: