Creating Webhooks
Creating a webhook is a two-step process. You'll first need to set up how you want your webhook to behave through CreativeForce - what events should it listen to. After that, you'll set up your server to receive and manage the payload.
Setting up a Webhook
To set up a webhook, go to the “Studio settings” page via the menu in the top right corner, and click on "Integration" and from there on the sub-menu "Webhooks":
From there, click “Add webhook”. Webhooks require a few configuration options before you can make use of them. We'll go through each of these settings below here.
Set up
Payload URL
The Payload URL is the URL of the server that will receive the webhook requests.
This URL must be HTTPS
The URL can not be an IP (private IP as well as public IP)
The URL can not contain any of the following:
localhost
creativeforce.io
metadata.google
burpcollaborator.net
.local
.internal
ram.aliyuncs.com
fuf.me
localtest.me
ulh.us
Example of a good Payload URL
Examples of bad Payload URLs
https://localhost:4567/payload
http://localhost:8000/payload
Secret
When your secret key is set, Creative Force uses it to create a hash signature with each payload. This hash signature is passed along with each request in the headers as X-CF-Signature. See the section about validating the webhook later in this document.
The format of the secret key is Base64 RFC 2045
Events to trigger the webhook
Events are at the core of webhooks. The webhooks fire whenever a certain action is taken either by users or the Creative Force system. Your server (payload URL) will intercept and act upon these.
You can select from 7 different event groups:
Product Events
Reset
StatusChanged
StyleGuideChanged
CategoryChanged
Ignored
ColorReferenceMigrated
PropertiesUpdated
Deleted
Activated
ProductStatusChangedviaKelvin
StyleguideChangedviaKelvin
CategoryChangedviaKelvin
ProductIgnoredviaKelvin
PropertiesUpdatedviaKelvin
ProductDeletedviaKelvin
ProductActivatedviaKelvin
EnableProductionType
DisableProductionType
ProductImported
ColorReferenceCreated
ColorReferenceUpdated
ColorReferenceDeleted
ProductCompleted
MoveToAnotherJob
ApplyUpdates
ProductAddedtoBundle
ProductRemovedfromBundle
ProductTriggeredCopywriting
ProductDeliveredCopywriting
ProductDeliveryCopywritingFailed
ProductIgnoreDeliverCopywriting
Sample Events
LocationChanged
Addedtocontainer
Releasedfromcontainer
Checked-in
Checked-out
User Events
UserLoggedIn
UserLoggedOut
PasswordChanged
AvatarUpdated
UserInvitationCompleted
UserUpdated
UserDeleted
UserInvitationDeleted
UserDisabled
UserEnabled
UserInvited
UserRe-Invited
ProductVendorAdded
StudioSign-UpCompleted
UserInvitationCompleted
UserInvitationDeleted
SettingMigrateColorReference(Ecomm)
SettingSourceE-Comm ColorReference(Editorial)
Task Events
Completed
Created
Started
Bypassed
Reset
Assigned
Unassigned
Rejected
Approved
WaitingforColorTeference
ProcesswithoutColorReference
Failed
CopywritingTaskEditMade
ReadyToWork
TaskSoftRejected
CopywritingDreemGenerateText
CopywritingDreemAdjustText
WorkUnit Events
WorkUnitStatusChanged
WorkUnitReset
WorkUnitEnabled
WorkUnitDisabled
WorkUnitChangedSource
WorkUnitCompleted
WorkUnitBypassExternalPost
WorkUnitChangePostVendor
WorkUnitCreated
WorkUnitDeleted
CopywritingProductionLayoutStateChanged
Job Events
StatusChanged
Created
Updated
Deleted
DeadlineUpdated
Session Events
AddToSession
RemovedfromSession
When you're finished, click Save. Now that you created the webhook, it's time to set up our server to test the webhook.
Webhook Details
Delivery headers
HTTP POST payloads that are delivered to your webhook's configured URL endpoint will contain several special headers:
Header | Description |
X-CF-Event | Name of the event that triggered the delivery. For example “Task completed”, “User created”. |
X-CF-Signature | The HMAC hex digest of the response body. See the section about validating the webhook later in this document. |
Webhook for “Product”
Key | Type | Description |
Action | string | The action that was performed. Can be one of: Reset StatusChanged StyleGuideChanged CategoryChanged Ignored ColorReferenceMigrated PropertiesUpdated Deleted Activated... |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. |
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: product |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
JobId | string | ID of the job (GUID) |
JobCode | string | Job code |
ClientId | string | ID of the client (GUID). |
ClientName | string | Client name |
ProductId | string | ID of the product (GUID) |
ProductCode | string | Product code |
ProductStatusId | int | Product Unit Status ID |
ProductStatusName | string | Product Unit Status Name |
Webhook payload example when someone reset a Product:
{
"Action":"ProductReset",
"Actor":{
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe"
},
"PayloadId":"c02f0f73-1706-4973-9a4d-38007d6685e9",
"EventGroupName":"product",
"EventDatetimeUtc":1595204418000,
"JobId":"d84eef10-c8a6-437a-9f95-0b58d2a3cb38",
"JobCode":"Summer 2020",
"ClientId":"2c856b2f-aded-4609-bf67-27009603b2b9",
"ClientName":"My Big client",
"ProductId":"7a782d57-b0bf-4743-8f02-2b1a1e229869",
"ProductCode":"AW2130146"
}
Webhook for “Sample”
Key | Type | Description |
Action | string | The action that was performed. Can be one for: LocationChanged AddedToContainer ReleasedFromContainer Checked-in Checked-out |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. |
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: sample |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
SampleId | string | ID of the Sample (GUID) |
SampleCode | String | Sample Code |
LocationId | string | ID of the location (GUID) |
LocationName | string | Location name |
SubLocationId | string | ID of the sub-location (GUID) |
SubLocationName | string | Sub-location name |
Products | array | Array of ID of the product ID (GUID) and Product code. If there is no product then return an empty array. products: [{productId: "", productCode: ""},....} |
Webhook payload example when someone check-in a sample:
{
"SampleId": "4666fcd0-0179-44d8-a66d-bc65a4acd968",
"SampleCode": "88829384",
"LocationId": "ee410fb0-6a99-4b0e-a95e-83abe6aa5a5e",
"LocationName": "CF Center",
"SubLocationId": "00000000-0000-0000-0000-000000000000",
"SubLocationName": "",
"Products": [{
"ProductId": "ef0533fc-a11c-43a2-ab84-1c9bdabc9e7f",
"ProductCode": "ADROAN-830"
}
],
"PayloadId": "5385f2a9-88da-4c38-88a5-5f6f875ecb74",
"EventGroupName":"sample",
"EventDatetimeUtc": 1601969236991,
"Action": "Checked-in",
"Actor": {
"UserId": "205c0bbd-ea29-4250-87f5-bdb254a91075"
}
}
Webhook for “User”
Key | Type | Description |
Action | string | The action that was performed. Can be one of: UserLoggedIn UserLoggedOut PasswordChanged AvatarUpdated ProductVendorAdded UserUpdated UserDeleted UserDisabled UserEnabled UserInvited... |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. |
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: user |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
UserId | string | ID of the user (GUID) Notes: We will send UserID only, no user name. |
Webhook payload example when a user login:
{
"Action":"UserLoggedIn",
"Actor":{
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe"
},
"PayloadId":"c02f0f73-1706-4973-9a4d-38007d6685e9",
"EventGroupName":"user",
"EventDatetimeUtc":1595204418000,
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe",
}
Webhook for “Task”
Key | Type | Description |
Action | string | The action that was performed. Can be one of: Reset Completed Started ReadyToWork Bypassed Assigned Unassigned Rejected Approved... |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. |
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: task |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
TaskId | string | ID of the task (GUID) |
Assignee | object | The assignee of this task. Notes: We will send UserID only, no user name. |
JobId | string | ID of the job (GUID) |
JobCode | string | Job code |
ClientId | string | ID of the client (GUID). |
ClientName | string | Client name |
ProductId | string | ID of the product (GUID) |
ProductCode | string | Product code |
StepId | int | ID of the step |
StepName | string | Step name |
StepStatusId | int | ID of the Step Status |
StepStatusName | string | Step Status name |
ShootingTypeId | int | ID of the Shooting type (aka Production type) |
ShootingTypeName | string | Shooting type name (aka Production type name) |
WorkflowId | string | ID of the Workflow |
WorkflowName | string | Workflow name |
StyleGuideId | string | ID of the Style guide |
StyleGuideName | string | Style guide name |
Webhook payload example when someone finishes a Photography task:
{
"Action":"completed",
"Actor":{
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe"
},
"PayloadId":"c02f0f73-1706-4973-9a4d-38007d6685e9",
"EventGroupName":"task",
"EventDatetimeUtc":1595204418000,
"TaskId":"c02f0f73-4665-4aae-aac1-c2fbc65365de",
"Assignee":{
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe"
},
"JobId":"d84eef10-c8a6-437a-9f95-0b58d2a3cb38",
"JobCode":"Summer 2020",
"ClientId":"2c856b2f-aded-4609-bf67-27009603b2b9",
"ClientName":"My Big client",
"ProductId":"7a782d57-b0bf-4743-8f02-2b1a1e229869",
"ProductCode":"AW2130146",
"StepId":"3",
"StepName":"Photography",
"StepStatusId":"9000",
"StepStatusName":"Done",
"ShootingTypeId":"1",
"ShootingTypeName":"On Model",
"WorkflowId":"2b5d3e1b-1706-4973-9a4d-57ab7d6685e9",
"WorkflowName":"My first workflow",
"StyleGuideId":"2376b9cb-67db-490f-a5e8-89811ed6b087",
"StyleGuideName":"Summer on the beach 2020"
}
Webhook for Work Unit
Key | Type | Description |
Action | string | The action that was performed. Can be one of: WorkUnitStatusChanged WorkUnitReset WorkUnitEnabled WorkUnitDisabled WorkUnitChangedSource WorkUnitCompleted WorkUnitBypassExternaPost WorkUnitChangePostVendor.... |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. For some following cases when data is updated by background service, then Actor is empty:
* Sync from data source: create/delete product
|
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: work unit |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
ClientId | string | ID of the client (GUID). |
ClientName | string | Client name |
ProductId | string | ID of the product (GUID) |
ProductCode | string | Product code |
ProductionTypeId | int | Production Type ID |
ProductionTypeName | string | Production Type Name |
WorkUnitStatusId | int | Work Unit Status ID after changed |
WorkUnitStatusName | string | Work Unit Status Name after changed |
Webhook payload example when a work unit status changed:
{
"Action":"StatusChanged",
"Actor":{
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe"
},
"PayloadId":"c02f0f73-1706-4973-9a4d-38007d6685e9",
"EventGroupName":"work unit",
"EventDatetimeUtc":1595204418000,
"ClientId":"2c856b2f-aded-4609-bf67-27009603b2b9",
"ClientName":"My Big client",
"ProductId":"7a782d57-b0bf-4743-8f02-2b1a1e229869",
"ProductCode":"AW2130146"
"ProductionTypeId":1,
"ProductionTypeName":"OnModel"
"WorkUnitStatusId":3000
"WorkUnitStatusName":"InProgress"
}
Webhooks for Jobs
Key | Type | Description |
Action | string | The action that was performed. Can be one of: Created, Updated, Deleted, StatusChanged, Deadline Updated |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. |
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: Job |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
JobId | string | ID of the job (GUID) |
JobName | string | Job name |
JobCode | string | Job code |
JobStatusId | int | Job Unit Status ID |
JobStatusName | string | Job Unit Status Name |
ClientId | string | ID of the client (GUID) |
ClientName | string | Client Name |
JobDeadlineUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
Webhook payload example when someone deletes a Job:
{
"Action":"Deleted",
"Actor":{
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe"
},
"PayloadId":"c02f0f73-1706-4973-9a4d-38007d6685e9",
"EventGroupName":"Job",
"EventDatetimeUtc":1595204418000,
"JobId":"d84eef10-c8a6-437a-9f95-0b58d2a3cb38",
“JobName:”Summer 2020”,
"JobCode":"Summer 2020”,
“JobStatusId”:3000,
“JobStatusName”:”In Progress”,
"ClientId":"2c856b2f-aded-4609-bf67-27009603b2b9",
"ClientName":"My Big client",
“JobDeadlineUtc”:1595204529000,
}
Webhook for Session
Key | Type | Description |
Action | string | The action that was performed. Can be one of: AddedToSession RemovedFromSession |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. |
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: session |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
ProductId | string | ID of the product (GUID) |
ProductCode | string | Product code |
ShootingTypeId | int | ID of the Shooting type (aka Production type) |
ShootingTypeName | string | Shooting type name (aka Production type name) |
WorkUnitId | string | ID of the WorkUnit (GUID) |
SessionId | string | ID of the Session (GUID) |
SessionCode | string | Session Code |
SessionName | string | Session Name |
PlannedTimeSlot | JSON literal | Time of Session |
PlannedTeam | JSON literal | PlannedTeam consists of individuals participating in that session and specifies who they are and their respective roles. |
PlannedProducerId | string | ID of the Planned Producer (GUID) |
PlannedProducerName | string | Planned Producer Name |
PlannedSetId | string | ID of the Planned Set (GUID) |
ProductionItemId
| string | ID of the Production Item (GUID) |
PlanningStatus
| JSON literal | Status (ID, Name) of the planning Can be one of: Unplanned = 2, Planned = 1 |
ActionSource | string | The app that triggered the event |
Webhook payload example when someone Add a Session:
{
"ActionSource": "AddFromGateway",
"SessionId": "cba7640d-452b-417b-8bbc-aaa92a763359",
"SessionCode": "SES113736CF",
"SessionName": "Quyen Test Add session",
"PlannedTimeSlot": "[{\"PlannedStart\":\"2023-12-10T01:00:00\",\"PlannedEnd\":\"2023-12-10T10:00:00\"}]",
"PlannedTeam": "{\"Assistant\":\"phuonglth+dev@creativeforce.io,dungnt+sing@creativeforce.io\",\"Photographer\":\"hanh@creativeforce.io\",\"HairAndMakeup\":\"duongvtt+dev@creativeforce.io\",\"DigitalProcessing\":\"hanttn@creativeforce.io\"}",
"PlannedProducerId": "429f37e8-4583-4541-89d9-49230bbf046f",
"PlannedProducerName": "DuongVTT DEV",
"PlannedSetId": "53c4bc2d-244a-490e-9f75-e3554246b6dd",
"ProductionItemId": "1e6b01e0-671e-462c-b266-692d6a16eb92",
"PlanningStatus": "{\"PlanningStatusId\":2,\"PlanningStatus\":\"Unplanned\"}",
"WorkUnitId": "1e6b01e0-671e-462c-b266-692d6a16eb92",
"ShootingTypeId": 1,
"ProductId": "99ecc381-41f1-4782-bcd4-e762e50a8a93",
"ProductCode": "Product_230831_5",
"PayloadId": "d483e7cb-e03b-4ccd-b307-22ca3e990e96",
"EventGroupName": "session",
"EventDatetimeUtc": 1701935735820,
"Action": "AddedToSession",
"Actor": {
"UserId": "600a5a14-8bd5-4d48-888e-f2b0bf2defc8"
}
}
Security
Validating the webhooks
The way to validate the webhook in your application is to hash the JSON payload of the webhook using the SHA256 algorithm and the secret key generated when you set up the webhook in gamma.
You will then compare the hash with the webhook signature. If they match, the webhook can be trusted.
The X-CF-Signature is 32 bytes and in hexa format. It's therefore important to make sure your hash function is returning 32-byte hexa as well.
Using a plain == operator is not advised. A method like secure_compare performs a "constant time" string comparison, which renders it safe from certain timing attacks against regular equality operators.
Below is an example of this process. The example is written in Ruby for a Sinatra server but should give you a good idea of the approach.
require 'sinatra'
require 'json'
post '/payload' do
request.body.rewind
payload_body = request.body.read
verify_signature(payload_body)
push = JSON.parse(payload_body)
puts "I got callback, JSON: #{push}"
return halt 200, "OK"
end
def verify_signature(payload_body)
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA256'), ENV['SECRET_TOKEN'], payload_body)
return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_CF_SIGNATURE'])
end
Other
Content-Type
Creative Force always uses application/json as the content type.
Enable/Disable
You can choose to disable a webhook if you want to pause it temporarily.
Retry
The CF system will POST data to the payload URL specified and expects a 2xx HTTP status code return. If no 2xx HTTP status is returned, CF system will automatically retry (3 times only), each after 5 - 10 minutes.